1. 시작하기 전에
이 Codelab에서는 Android용 Maps SDK를 앱과 통합하고 미국 캘리포니아주 샌프란시스코에 있는 자전거 매장을 표시하는 앱을 빌드하여 핵심 기능을 사용하는 방법을 알려줍니다.
기본 요건
- Kotlin 및 Android 개발에 대한 기본 지식
수행할 작업
- Android용 Maps SDK를 사용하여 Android 앱에 Google 지도를 추가합니다.
- 마커를 추가, 맞춤설정 및 클러스터링합니다.
- 지도에 다중선과 다각형을 그립니다.
- 프로그래밍 방식으로 카메라의 시점을 조정합니다.
필요한 항목
- Android용 Maps SDK
- 결제가 사용 설정된 Google 계정
- Android 스튜디오 2020.3.1 이상
- Android 스튜디오에 Google Play 서비스 설치
- Android 4.2.2 이상 기반의 Google API 플랫폼을 실행하는 Android 기기 또는 Android Emulator(설치 단계는 Android Emulator에서 앱 실행 참조)
2. 설정
다음 사용 설정 단계를 진행하려면 Android용 Maps SDK를 사용 설정해야 합니다.
Google Maps Platform 설정하기
Google Cloud Platform 계정 및 결제가 사용 설정된 프로젝트가 없는 경우 Google Maps Platform 시작하기 가이드를 참고하여 결제 계정 및 프로젝트를 만듭니다.
- Cloud Console에서 프로젝트 드롭다운 메뉴를 클릭하고 이 Codelab에 사용할 프로젝트를 선택합니다.
- Google Cloud Marketplace에서 이 Codelab에 필요한 Google Maps Platform API 및 SDK를 사용 설정합니다. 이 동영상 또는 이 문서의 단계를 따릅니다.
- Cloud Console의 Credentials 페이지에서 API 키를 생성합니다. 이 동영상 또는 이 문서의 단계를 따릅니다. Google Maps Platform에 대한 모든 요청에는 API 키가 필요합니다.
3. 빠른 시작
빠르게 시작할 수 있도록 이 Codelab을 따라하는 데 도움이 되는 시작 코드가 있습니다. 해법으로 바로 넘어갈 수 있지만 모든 단계를 따라하면서 직접 빌드하려면 계속 읽으시기 바랍니다.
git
을 설치한 경우 저장소를 클론합니다.
git clone https://github.com/googlecodelabs/maps-platform-101-android.git
또는 다음 버튼을 클릭하여 소스 코드를 다운로드할 수도 있습니다.
- 코드를 받으면 Android 스튜디오의
starter
디렉터리 내에 있는 프로젝트를 엽니다.
4. Google 지도 추가하기
이 섹션에서는 앱을 실행할 때 로드되도록 Google 지도를 추가합니다.
API 키 추가하기
이전 단계에서 만든 API 키를 앱에 제공해야 Android용 Maps SDK에서 키를 앱과 연결할 수 있습니다.
- API 키를 제공하려면 프로젝트의 루트 디렉터리(
gradle.properties
및settings.gradle
과 동일한 수준)에서local.properties
라는 파일을 엽니다. - 이 파일에서 내가 만든 API 키를 값으로 갖는 새 키
GOOGLE_MAPS_API_KEY
를 정의합니다.
local.properties
GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE
local.properties
는 Git 저장소의 .gitignore
파일에 나열되어 있습니다. 이는 API 키가 민감한 정보로 간주되고 가급적 소스 제어에 체크인하면 안 되기 때문입니다.
- 다음으로, 앱 전체에서 사용할 수 있도록 API를 노출하려면
app/
디렉터리에 있는 앱의build.gradle
파일에 Secrets Gradle Plugin for Android 플러그인을 포함하고plugins
블록 내에 다음 줄을 추가합니다.
앱 수준 build.gradle
plugins {
// ...
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}
또한 다음 클래스 경로를 포함하도록 프로젝트 수준의 build.gradle
파일을 수정해야 합니다.
프로젝트 수준 build.gradle
buildscript {
dependencies {
// ...
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:1.3.0"
}
}
이 플러그인은 local.properties
파일 내에서 정의한 키를 Android 매니페스트 파일의 빌드 변수로 제공하고 빌드 시 이를 Gradle에서 생성된 BuildConfig
클래스의 변수로 사용할 수 있게 합니다. 이 플러그인을 사용하면 local.properties
에서 속성을 읽는 데 필요한 상용구 코드가 제거되어 앱 전체에서 액세스할 수 있습니다.
Google 지도 종속 항목 추가하기
- 앱 내에서 API 키에 액세스할 수 있게 되었으므로, 다음 단계는 Android용 Maps SDK 종속 항목을 앱의
build.gradle
파일에 추가하는 것입니다.
이 Codelab과 함께 제공되는 시작 프로젝트에서는 이 종속 항목이 이미 추가되어 있습니다.
build.gradle
dependencies {
// Dependency to include Maps SDK for Android
implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
- 그런 다음
AndroidManifest.xml
에 새meta-data
태그를 추가하여 이전 단계에서 만든 API 키를 전달합니다. 이렇게 하려면 Android 스튜디오에서 이 파일을 열고app/src/main
에 있는AndroidManifest.xml
파일의application
객체 내에 다음meta-data
태그를 추가합니다.
AndroidManifest.xml
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${GOOGLE_MAPS_API_KEY}" />
- 다음으로,
app/src/main/res/layout/
디렉터리에activity_main.xml
이라는 새 레이아웃 파일을 만들고 다음과 같이 정의합니다.
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>
이 레이아웃에는 SupportMapFragment
가 포함된 단일 FrameLayout
이 있습니다. 이 프래그먼트에는 이후 단계에서 사용하는 기본 GoogleMaps
객체가 포함됩니다.
- 마지막으로,
app/src/main/java/com/google/codelabs/buildyourfirstmap
에 있는MainActivity
클래스를 업데이트합니다. 다음 코드를 추가하여onCreate
메서드를 재정의하면 방금 만든 새 레이아웃으로 콘텐츠를 설정할 수 있습니다.
MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
- 앱을 실행하면 기기 화면에 지도가 로드됩니다.
5. 클라우드 기반 지도 스타일 지정 (선택사항)
클라우드 기반 지도 스타일 지정을 사용하여 지도 스타일을 맞춤설정할 수 있습니다.
지도 ID 만들기
연결된 지도 스타일이 있는 지도 ID를 아직 만들지 않은 경우 지도 ID 가이드를 참고하여 다음 단계를 완료하세요.
- 지도 ID 만들기
- 지도 ID를 지도 스타일에 연결하기
앱에 지도 ID 추가하기
만든 지도 ID를 사용하려면 activity_main.xml
파일을 수정하고 SupportMapFragment
의 map:mapId
속성에 지도 ID를 전달합니다.
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" />
그런 다음 앱을 실행하여 선택한 스타일로 지도를 확인합니다.
6. 마커 추가하기
이 작업을 통해 지도에서 강조하려는 관심 장소를 나타내는 마커를 지도에 추가합니다. 먼저 시작 프로젝트에 제공된 장소 목록을 검색한 다음, 해당 장소를 지도에 추가합니다. 이 예에서는 자전거 매장입니다.
GoogleMap 참조 가져오기
먼저 메서드를 사용할 수 있도록 GoogleMap
객체에 대한 참조를 가져와야 합니다. 이렇게 하려면 setContentView()
호출 바로 다음에 있는 MainActivity.onCreate()
메서드에 다음 코드를 추가하세요.
MainActivity.onCreate()
val mapFragment = supportFragmentManager.findFragmentById(
R.id.map_fragment
) as? SupportMapFragment
mapFragment?.getMapAsync { googleMap ->
addMarkers(googleMap)
}
이 구현은 먼저 SupportFragmentManager
객체의 findFragmentById()
메서드를 사용하여 이전 단계에서 추가한 SupportMapFragment
를 찾습니다. 참조를 가져오면 getMapAsync()
호출 후에 람다를 전달합니다. 이 람다는 GoogleMap
객체가 전달되는 위치입니다. 이 람다 내부에서는 곧 정의될 addMarkers()
메서드 호출이 이루어집니다.
제공된 클래스: PlacesReader
시작 프로젝트에는 PlacesReader
클래스가 제공되어 있습니다. 이 클래스는 places.json
이라는 JSON 파일에 저장된 49개 장소의 목록을 읽고 List<Place>
로 반환합니다. 장소는 미국 캘리포니아주 샌프란시스코에 있는 자전거 매장 목록을 나타냅니다.
이 클래스의 구현에 관해 궁금한 점이 있다면 GitHub에서 액세스하거나 Android 스튜디오에서 PlacesReader
클래스를 열면 됩니다.
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()
}
}
장소 로드하기
자전거 매장 목록을 로드하려면 MainActivity
에 places
라는 속성을 추가하고 다음과 같이 정의합니다.
MainActivity.places
private val places: List<Place> by lazy {
PlacesReader(this).read()
}
이 코드는 List<Place>
를 반환하는 PlacesReader
의 read()
메서드를 호출합니다. Place
에는 name
이라는 속성, 장소의 이름, latLng
(장소가 위치한 곳의 좌표)가 있습니다.
장소
data class Place(
val name: String,
val latLng: LatLng,
val address: LatLng,
val rating: Float
)
지도에 마커 추가하기
이제 장소 목록이 메모리에 로드되었으므로 다음 단계는 지도에 이러한 장소를 나타내는 것입니다.
MainActivity
에addMarkers()
라는 메서드를 만들고 다음과 같이 정의합니다.
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)
)
}
}
이 메서드는 places
목록을 반복해서 처리한 후 제공된 GoogleMap
객체에서 addMarker()
메서드를 호출합니다. 마커는 MarkerOptions
객체를 인스턴스화하여 생성되므로 마커 자체를 맞춤설정할 수 있습니다. 이 경우, 마커의 제목과 위치가 제공되며, 이는 각각 자전거 매장 이름과 해당 좌표를 나타냅니다.
- 앱을 실행하고 샌프란시스코로 이동하여 방금 추가한 마커를 확인하세요.
7. 마커 맞춤설정하기
방금 추가한 마커에 대한 여러 맞춤설정 옵션을 통해 마커를 돋보이게 만들고 사용자에게 유용한 정보를 전달할 수 있습니다. 이 작업에서는 각 마커의 이미지와 마커를 탭할 때 표시되는 정보 창을 맞춤설정하여 해당 옵션 중 일부를 살펴봅니다.
정보 창 추가하기
기본적으로 마커를 탭하면 정보 창에 제목과 스니펫이 표시됩니다(설정된 경우). 장소의 주소 및 평점과 같은 추가 정보를 표시하도록 맞춤설정할 수 있습니다.
marker_info_contents.xml 만들기
먼저 marker_info_contents.xml
이라는 새 레이아웃 파일을 만듭니다.
- 이렇게 하려면 Android 스튜디오의 프로젝트 뷰에서
app/src/main/res/layout
폴더를 마우스 오른쪽 버튼으로 클릭하고 새로 만들기 > 레이아웃 리소스 파일을 선택합니다.
- 대화상자의 파일 이름 필드에
marker_info_contents
를 입력하고Root element
필드에LinearLayout
을 입력한 다음 확인을 클릭합니다.
이 레이아웃 파일은 나중에 정보 창에 있는 내용을 나타내도록 확장됩니다.
- 세로
LinearLayout
뷰 그룹 내에 세 개의TextViews
를 추가하는 다음 코드 스니펫의 내용을 복사하여 파일의 기본 코드에 덮어씁니다.
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>
InfoWindowAdapter 구현 만들기
맞춤 정보 창의 레이아웃 파일을 만든 후 다음 단계는 GoogleMap.InfoWindowAdapter 인터페이스를 구현하는 것입니다. 이 인터페이스에는 getInfoWindow()
와 getInfoContents()
라는 두 가지 메서드가 포함됩니다. 두 메서드 모두 선택 사항인 View
객체를 반환하는데, 여기에서 전자는 창 자체를 맞춤설정하는 데 사용되고 후자는 내용을 맞춤설정하는 데 사용됩니다. 이 경우에는 getInfoWindow()
에서 null을 반환하는 동안 두 가지를 모두 구현하고 getInfoContents()
의 반환을 맞춤설정합니다. 이는 기본 창을 사용해야 함을 나타냅니다.
- Android 스튜디오에서 프로젝트 뷰의
app/src/main/java/com/google/codelabs/buildyourfirstmap
폴더를 마우스 오른쪽 버튼으로 클릭한 다음 새로 만들기 > Kotlin 파일/클래스를 선택하여MainActivity
와 동일한 패키지에MarkerInfoWindowAdapter
라는 새 Kotlin 파일을 만듭니다.
- 대화상자에서
MarkerInfoWindowAdapter
를 입력하고 파일이 강조 표시된 상태를 유지합니다.
- 파일을 만들었으면 다음 코드 스니펫의 내용을 새 파일에 복사합니다.
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
}
}
getInfoContents()
메서드의 내용에서, 메서드에 제공된 마커는 Place
유형으로 변환되며, 변환이 불가능한 경우 메서드는 null을 반환합니다(Marker
에 태그 속성을 아직 설정하지 않았지만 다음 단계에서 이를 설정합니다).
다음으로, marker_info_contents.xml
레이아웃이 확장되고 TextViews
포함에 관한 텍스트가 Place
태그로 설정됩니다.
MainActivity 업데이트하기
지금까지 만든 모든 구성요소를 연결하려면 MainActivity
클래스에 두 줄을 추가해야 합니다.
먼저 getMapAsync
메서드 호출 내에 맞춤 InfoWindowAdapter
, MarkerInfoWindowAdapter
를 전달하려면 GoogleMap
객체의 setInfoWindowAdapter()
메서드를 호출하고 MarkerInfoWindowAdapter
의 새 인스턴스를 만듭니다.
getMapAsync()
람다 내에 있는addMarkers()
메서드 호출 뒤에 다음 코드를 추가하여 이 작업을 수행합니다.
MainActivity.onCreate()
// Set custom info window adapter
googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
마지막으로, 지도에 추가된 모든 마커에서 각 장소를 태그 속성으로 설정해야 합니다.
- 이렇게 하려면
addMarkers()
함수의places.forEach{}
호출을 다음과 같이 수정합니다.
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
}
맞춤 마커 이미지 추가하기
마커 이미지 맞춤설정은 지도에서 마커가 나타내는 장소의 유형을 재미있게 전달하는 방식 중 하나입니다. 이 단계에서는 지도에 각 매장을 나타내는 기본 빨간색 마커 대신 자전거를 표시합니다. 시작 프로젝트에는 app/src/res/drawable
에 자전거 아이콘 ic_directions_bike_black_24dp.xml
이 포함되어 있으며, 이를 사용하게 됩니다.
마커에 맞춤 비트맵 설정하기
벡터 드로어블 자전거 아이콘을 사용하는 경우, 다음 단계는 해당 드로어블을 지도에 있는 각 마커의 아이콘으로 설정하는 것입니다. MarkerOptions
에는 이 작업을 처리하는 데 사용하는 BitmapDescriptor
에서 취하는 icon
메서드가 있습니다.
먼저 방금 추가한 벡터 드로어블을 BitmapDescriptor
로 변환해야 합니다. 시작 프로젝트에 포함된 BitMapHelper
라는 파일에는 이 작업을 처리하는 vectorToBitmap()
이라는 도우미 함수가 포함되어 있습니다.
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)
}
}
이 메서드는 Context
, 드로어블 리소스 ID, 색상 정수를 사용하며, 메서드의 BitmapDescriptor
표현을 만듭니다.
도우미 메서드를 사용하여 bicycleIcon
이라는 새 속성을 선언하고 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)
}
이 속성은 앱에서 사전 정의된 색상 colorPrimary
를 사용하여 자전거 아이콘에 색조를 적용하거나 BitmapDescriptor
로 반환합니다.
- 이 속성을 사용하여 아이콘 맞춤설정을 완료하려면
addMarkers()
메서드에서MarkerOptions
의icon
메서드를 호출합니다. 그러면 마커 속성이 다음과 같이 됩니다.
MainActivity.addMarkers()
val marker = googleMap.addMarker(
MarkerOptions()
.title(place.name)
.position(place.latLng)
.icon(bicycleIcon)
)
- 앱을 실행하여 업데이트된 마커를 확인하세요.
8. 마커 클러스터링
지도를 확대/축소하는 정도에 따라 추가한 마커가 겹칠 수 있습니다. 중첩되어 있는 마커는 상호작용하기가 매우 까다로우며 많은 노이즈를 생성하므로 앱의 사용성에 영향을 미칩니다.
이와 관련한 사용자 환경을 개선하려면 밀집하여 클러스터링된 큰 데이터 세트가 있을 때마다 마커 클러스터링을 구현하는 것이 좋습니다. 클러스터링을 사용하면 지도를 확대/축소할 때 가까이에 있는 마커가 다음과 같이 함께 클러스터링됩니다.
이 기능을 구현하려면 Android용 Maps SDK 유틸리티 라이브러리가 필요합니다.
Android용 Maps SDK 유틸리티 라이브러리
Android용 Maps SDK 유틸리티 라이브러리는 Android용 Maps SDK의 기능을 확장하기 위한 방편으로 생성되었습니다. 이는 마커 클러스터링, 히트맵, KML 및 GeoJson 지원, 다중선 인코딩 및 디코딩, 구면 기하학에 대한 몇몇 도우미 함수와 같은 고급 기능을 제공합니다.
build.gradle 업데이트하기
유틸리티 라이브러리는 Android용 Maps SDK와 별도로 패키징되므로 build.gradle
파일에 추가 종속 항목을 추가해야 합니다.
app/build.gradle
파일의dependencies
섹션을 업데이트합니다.
build.gradle
implementation 'com.google.maps.android:android-maps-utils:1.1.0'
- 이 줄을 추가한 후에는 프로젝트 동기화를 수행하여 새 종속 항목을 가져와야 합니다.
클러스터링 구현하기
앱에서 클러스터링을 구현하려면 다음 3가지 단계를 따르세요.
ClusterItem
인터페이스를 구현합니다.DefaultClusterRenderer
클래스의 서브클래스를 만듭니다.ClusterManager
를 만들고 항목을 추가합니다.
ClusterItem 인터페이스 구현하기
지도에서 클러스터형 마커를 나타내는 모든 객체는 ClusterItem
인터페이스를 구현해야 합니다. 이 경우, Place
모델이 ClusterItem
을 준수해야 합니다. Place.kt
파일을 열어 다음과 같이 수정합니다.
장소
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은 다음 세 가지 메서드를 정의합니다.
getPosition()
- 장소의LatLng
를 나타냅니다.getTitle()
- 장소의 이름을 나타냅니다.getSnippet()
- 장소의 주소를 나타냅니다.
DefaultClusterRenderer 클래스의 서브클래스 만들기
클러스터링을 구현하는 클래스인 ClusterManager
는 화면을 이동하거나 지도를 확대/축소할 때 내부적으로 ClusterRenderer
클래스를 사용하여 클러스터 생성을 처리합니다. 기본적으로 ClusterRenderer
를 구현하는 기본 렌더기인 DefaultClusterRenderer
가 제공됩니다. 간단한 경우에는 이 렌더기로 충분합니다. 그러나 이 경우에는 마커를 맞춤설정해야 하므로 이 클래스를 확장하고 여기에 맞춤설정을 추가해야 합니다.
com.google.codelabs.buildyourfirstmap.place
패키지에서 Kotlin 파일 PlaceRenderer.kt
를 만들고 다음과 같이 정의합니다.
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
}
}
이 클래스는 다음 두 함수를 재정의합니다.
onBeforeClusterItemRendered()
: 클러스터가 지도에 렌더링되기 전에 호출됩니다. 여기에서MarkerOptions
를 통해 맞춤설정을 제공할 수 있으며, 이 경우에는 마커의 제목, 위치 및 아이콘이 설정됩니다.onClusterItemRenderer()
: 지도에서 마커가 렌더링된 직후 호출됩니다. 여기에서 생성된Marker
객체에 액세스할 수 있으며, 이 경우에는 마커의 태그 속성을 설정합니다.
ClusterManager를 만들고 항목 추가하기
마지막으로 클러스터링을 사용하려면 MainActivity
를 수정하여 ClusterManager
를 인스턴스화하고 필요한 종속 항목을 제공해야 합니다. ClusterManager
는 마커(ClusterItem
객체)를 내부적으로 추가하므로 마커를 지도에 직접 추가하는 대신 이 책임이 ClusterManager
에 위임됩니다. 또한 ClusterManager
는 내부적으로 setInfoWindowAdapter()
도 호출하므로 ClusterManger
의 MarkerManager.Collection
객체에서 맞춤 정보 창을 설정해야 합니다.
- 시작하려면
MainActivity.onCreate()
의getMapAsync()
호출에서 람다의 내용을 수정합니다.addMarkers()
및setInfoWindowAdapter()
호출을 주석 처리하고 대신 다음 단계에서 정의하는addClusteredMarkers()
라는 메서드를 호출합니다.
MainActivity.onCreate()
mapFragment?.getMapAsync { googleMap ->
//addMarkers(googleMap)
addClusteredMarkers(googleMap)
// Set custom info window adapter.
// googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
- 다음으로,
MainActivity
에서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()
}
}
이 메서드는 ClusterManager
를 인스턴스화하고 여기에 맞춤 렌더기 PlacesRenderer
를 전달하고, 모든 장소를 추가하고, cluster()
메서드를 호출합니다. 또한 ClusterManager
는 지도 객체에서 setInfoWindowAdapter()
메서드를 사용하므로 ClusterManager.markerCollection
객체에서 맞춤 정보 창을 설정해야 합니다. 마지막으로 사용자가 화면을 이동하고 지도를 확대/축소할 때 클러스터링이 변경되도록 해야 하므로, 카메라가 유휴 상태가 되면 clusterManager.onCameraIdle()
이 호출되도록 googleMap
에 OnCameraIdleListener
가 제공됩니다.
- 앱을 실행하여 새로운 클러스터링된 매장을 확인하세요.
9. 지도에 그리기
마커를 추가하여 지도에 그리는 한 가지 방법을 알아보았는데, Android용 Maps SDK는 지도에 유용한 정보를 표시하기 위해 그릴 수 있는 여러 가지 다른 방법을 지원합니다.
예를 들어 지도에 경로와 영역을 나타내려면 다중선 및 다각형을 사용하여 지도에 표시할 수 있습니다. 또는 지표면에 이미지를 고정하려는 경우 지면 오버레이를 사용할 수 있습니다.
이 작업에서는 마커를 탭할 때마다 그 주변에 도형(원)을 그리는 방법을 알아봅니다.
클릭 리스너 추가하기
일반적으로 클릭 리스너를 마커에 추가하는 방법은 setOnMarkerClickListener()
를 통해 GoogleMap
객체에 직접 클릭 리스너를 전달하는 것입니다. 하지만 클러스터링을 사용 중이므로 대신 클릭 리스너를 ClusterManager
에 제공해야 합니다.
MainActivity
의addClusteredMarkers()
메서드에서cluster()
에 대한 호출 바로 뒤에 다음 줄을 추가합니다.
MainActivity.addClusteredMarkers()
// Show polygon
clusterManager.setOnClusterItemClickListener { item ->
addCircle(googleMap, item)
return@setOnClusterItemClickListener false
}
이 메서드는 리스너를 추가하고 다음 단계에서 정의하는 addCircle()
메서드를 호출합니다. 마지막으로, 이 메서드에서 false
가 반환되어 이 메서드가 이 이벤트를 사용하지 않았음을 나타냅니다.
- 다음으로
MainActivity
에circle
속성과addCircle()
메서드를 정의해야 합니다.
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))
)
}
circle
속성이 설정되어 새 아이콘을 탭할 때마다 이전 원이 삭제되고 새 원이 추가됩니다. 원을 추가하기 위한 API는 마커를 추가하는 것과 매우 유사합니다.
- 지금 앱을 실행하여 변경사항을 확인하세요.
10. 카메라 제어
마지막 작업으로, 특정 지역을 중심으로 초점을 맞출 수 있는 몇 가지 카메라 제어 방식을 살펴봅니다.
카메라 및 뷰
앱을 실행하면 카메라가 아프리카 대륙을 표시합니다. 이때 추가한 마커를 찾으려면 번거롭게 샌프란시스코까지 화면을 이동한 후 확대/축소해야 합니다. 이는 세상을 탐색하는 재미있는 방법일 수 있지만 마커를 바로 표시하고자 하는 경우에는 유용하지 않습니다.
이를 위해 카메라의 위치를 프로그래밍을 통해 설정하여 뷰가 원하는 곳의 중앙에 위치하게 할 수 있습니다.
- 앱이 실행될 때 카메라 뷰를 조정하여 샌프란시스코로 초기화되도록
getMapAsync()
호출에 다음 코드를 추가해 보세요.
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))
}
}
먼저 지도가 로드된 후에만 카메라가 업데이트되도록 setOnMapLoadedCallback()
이 호출됩니다. 카메라 업데이트를 호출하기 전에 크기와 같은 지도 속성을 계산해야 하므로 이 단계가 필요합니다.
람다에는 지도의 직사각형 영역을 정의하는 새로운 LatLngBounds
객체가 생성됩니다. 이는 모든 장소가 경계 내에 있도록 하기 위해 모든 장소의 LatLng
값을 포함하는 방식으로 점진적으로 빌드됩니다. 이 객체가 빌드되면 GoogleMap
의 moveCamera()
메서드가 호출되고 CameraUpdateFactory.newLatLngBounds(bounds.build(), 20)
을 통해 여기에 CameraUpdate
가 제공됩니다.
- 앱을 실행하고 카메라가 이제 샌프란시스코로 초기화되는지 확인합니다.
카메라 변경사항 수신하기
카메라 위치를 수정하는 것 외에도, 사용자가 지도를 탐색할 때 카메라 업데이트를 수신할 수도 있습니다. 이 기능은 카메라가 이동할 때 UI를 수정하려는 경우에 유용할 수 있습니다.
재미를 위해 카메라가 움직일 때마다 마커가 반투명해지도록 코드를 수정합니다.
addClusteredMarkers()
메서드에서 메서드 하단에 다음 줄을 추가합니다.
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 }
}
그러면 OnCameraMoveStartedListener
가 추가되어 카메라가 움직이기 시작하면 모든 마커(클러스터와 마커 모두)의 알파 값이 0.3f
로 수정되어 마커가 반투명으로 표시됩니다.
- 마지막으로, 카메라가 멈출 때 반투명 마커를 다시 불투명으로 수정하려면
addClusteredMarkers()
메서드에서setOnCameraIdleListener
의 내용을 다음과 같이 수정합니다.
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()
}
- 앱을 실행하여 결과를 확인하세요.
11. 지도 KTX
Google Maps Platform Android SDK를 하나 이상 사용하는 Kotlin 앱의 경우 Kotlin 확장 프로그램이나 KTX 라이브러리를 사용하여 코루틴, 확장 속성/함수 등과 같은 Kotlin 언어 기능을 활용할 수 있습니다. 각 Google Maps SDK에는 아래와 같이 해당하는 KTX 라이브러리가 있습니다.
이 작업에서는 지도 KTX 및 지도 유틸리티 KTX 라이브러리를 앱에 사용하고 이전 작업을 리팩터링하여 앱에서 Kotlin 관련 언어 기능을 사용할 수 있도록 합니다.
- 앱 수준 build.gradle 파일에 KTX 종속 항목 포함
앱은 Android용 Maps SDK와 Android용 Maps SDK 유틸리티 라이브러리를 모두 사용하므로 이 라이브러리에 해당하는 KTX 라이브러리를 포함해야 합니다. 또한 이 작업에서 AndroidX Lifecycle KTX 라이브러리에 있는 기능을 사용하므로 이 종속 항목을 앱 수준 build.gradle
파일에도 포함해야 합니다.
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'
}
- GoogleMap.addMarker() 및 GoogleMap.addCircle() 확장 함수 사용
지도 KTX 라이브러리는 이전 단계에서 사용된 GoogleMap.addMarker(MarkerOptions)
및 GoogleMap.addCircle(CircleOptions)
을 위한 DSL 스타일 API 대안을 제공합니다. 앞서 언급한 API를 사용하려면 마커 또는 원 옵션이 포함된 클래스를 구성해야 합니다. KTX 대체 옵션을 사용하면 제공된 람다에 마커 또는 원 옵션을 설정할 수 있습니다.
이러한 API를 사용하려면 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))
}
}
위의 방식으로 이러한 메서드를 다시 쓰면 훨씬 더 간결해지며 Kotlin의 수신자가 있는 함수 리터럴을 사용하면 가능합니다.
- SupportMapFragment.awaitMap() 및 GoogleMap.awaitMapLoad() 확장 정지 함수 사용
지도 KTX 라이브러리는 코루틴 내에서 사용할 정지 함수 확장도 제공합니다. 특히 SupportMapFragment.getMapAsync(OnMapReadyCallback)
및 GoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback)
의 정지 함수 대안이 있습니다. 이러한 대체 API를 사용하면 콜백을 전달할 필요가 없으며 그 대신 이러한 메서드의 응답을 직렬 및 동기 방식으로 수신할 수 있습니다.
이러한 메서드는 정지 함수이므로 코루틴 내에서 메서드를 사용해야 합니다. Lifecycle 런타임 KTX 라이브러리는 수명 주기를 인식하는 코루틴 범위를 제공하는 확장 프로그램을 제공하므로 적절한 수명 주기 이벤트에서 코루틴이 실행되고 중지됩니다.
이러한 개념을 조합하여 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)
}
}
lifecycleScope.launchWhenCreated
코루틴 범위는 활동이 최소한 생성된 상태일 때 블록을 실행합니다. 또한 GoogleMap
객체를 검색하고 지도 로드가 완료될 때까지 대기하는 호출이 각각 SupportMapFragment.awaitMap()
및 GoogleMap.awaitMapLoad()
로 대체되었습니다. 이러한 정지 함수를 사용하여 코드를 리팩터링하면 상응하는 콜백 기반 코드를 순차적으로 작성할 수 있습니다.
- 리팩터링된 변경사항으로 앱을 다시 빌드하세요.
12. 축하합니다
수고하셨습니다 많은 내용을 살펴봤습니다. 이제 Android용 Maps SDK에서 제공하는 핵심 기능에 대해 더 잘 이해하셨기를 바랍니다.
자세히 알아보기
- Android용 Places SDK: 풍부한 장소 데이터 세트를 살펴보고 주변의 비즈니스를 탐색합니다.
- android-maps-ktx: Android용 Maps SDK 및 Android용 Maps SDK 유틸리티 라이브러리를 Kotlin 친화적인 방식으로 통합할 수 있는 오픈소스 라이브러리입니다.
- android-place-ktx: Kotlin 친화적인 방식으로 Android용 Places SDK와 통합할 수 있는 오픈소스 라이브러리
- android-samples: 이 Codelab 등에서 다룬 모든 기능을 보여주는 GitHub의 샘플 코드입니다.
- Google Maps Platform으로 Android 앱을 빌드하기 위한 추가 Kotlin Codelab