구글맵에 커스텀 마커와 클러스터아이템 적용하기

이번 포스팅에서는 구글맵 마커를 커스텀하고 클러스터링하는 법에 대해 알아보도록 하겠습니다.

다음과 같이 좁은 공간에 마커가 모여있을 경우 마커를 뭉쳐서 숫자로 변환하는 것을 클러스터링이라고 합니다. 지도를 확대하면 클러스터가 개별 마커로 펼쳐지기 때문에 많은 마커를 표시해야 하는 경우에 공간을 효율적으로 사용할 수 있습니다.

출처 : https://developers.google.com/maps/documentation/android-sdk/utility/marker-clustering

출처 : https://developers.google.com/maps/documentation/android-sdk/utility/marker-clustering

여기서는 기존에 작성했던 구글맵 프로젝트에 코로나 선별진료소를 표시하면서 클러스터링을 구현해보도록 하겠습니다.

# Dependency 추가

build.gradle에 클러스터링에 사용할 클래스를 담은 android-maps-utils 를 추가합니다.

1
2
3
dependencies {
    implementation 'com.google.maps.android:android-maps-utils:2.3.0'
}

# 선별진료소 데이터 준비

마커에 사용할 선별진료소 데이터는 경기데이터드림에서 제공하는 코로나바이러스 감염증 선별진료소 운영 의료기관 현황데이터를 사용하겠습니다. 이 데이터에는 마커에 바로 사용할 수 있는 경도와 위도정보가 포함되어 있기 때문에 이번 강의의 제물로 선택되었습니다.

sheet 탭에서 JSON 을 클릭하여 데이터를 json 형식으로 다운받습니다. 다운받아보면 내용이 한줄에 모여있어 보기 불편합니다. 사용하는데는 관계 없지만 값들을 정렬하는게 보기에 좋을 것 같으니 Json Formatter 에서 파일을 정렬하고 data.json이라는 이름으로 assets 폴더에 저장합니다.

앱에서 json 파일을 편하게 다루기 위해 gson 라이브러리를 추가하고, 데이터를 오브젝트로 변환하기 위한 DAO를 작성합니다. JSON To Kotlin Class 플러그인을 사용하면 DAO를 편하게 만들 수 있습니다.

1
2
3
dependencies {
    implementation 'com.google.code.gson:gson:2.8.6'
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class CoronaMed : ArrayList<CoronaMedItem>()

data class CoronaMedItem(
    val AGRGT_DE: String, // 2021-04-01
    val DISTRCT_DIV_DTLS: String, // 호흡기전담클리닉
    val EMGC_CENTER_TELNO: String, // 031-8003-8275
    val MEDINST_NM: String, // 상쾌한이비인후과의원
    val REFINE_LOTNO_ADDR: String, // 경기도 화성시 반송동 216-1번지
    val REFINE_ROADNM_ADDR: String, // 경기도 화성시 동탄솔빛로 44
    val REFINE_WGS84_LAT: String, // 37.1928811270
    val REFINE_WGS84_LOGT: String, // 127.0729965789
    val REPRSNT_TELNO: String,
    val SIGUN_NM: String // 화성시
)

마지막으로 asset 폴더에서 불러온 데이터를 DAO로 변환하는 함수를 작성합니다. 함수의 자세한 설명은 Assets 폴더에서 로딩한 json 파일 Recyclerview에 표시하기 강의를 참조하시기 바랍니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private fun getJsonData(filename: String): CoronaMed {
    var result = CoronaMed()

    try {
        val assetManager = resources.assets
        val inputStream = assetManager.open(filename)
        val reader = inputStream.bufferedReader()
        val gson = Gson()
        result = gson.fromJson(reader, CoronaMed::class.java)
    } catch (e: Exception) {
        e.printStackTrace()
    }
    return result
}

# 마커 준비

데이터 준비가 끝났으니 이젠 마커를 준비하겠습니다. 우선은 마커로 사용할 아이콘을 설정해줍니다. 코로나 선별진료소를 표시할 것이므로 적십자 마크를 사용하도록 하겠습니다. 다음 png 파일을 drawable 폴더에 복사하여 넣어줍니다.

출처 : https://commons.wikimedia.org/wiki/File:Red_Cross_icon.svg

출처 : https://commons.wikimedia.org/wiki/File:Red_Cross_icon.svg

이 이미지파일은 createScaledBitmap 을 통해 다음과 같이 비트맵으로 변환해서 마커를 렌더링할때 사용합니다.

1
2
3
4
5
private val mapIcon by lazy {
    val drawable =
        ResourcesCompat.getDrawable(resources, R.drawable.mapicon, null) as BitmapDrawable
    Bitmap.createScaledBitmap(drawable.bitmap, 64, 64, false)
}

# 클러스터 아이템 구현

클러스터링을 구현하기 위해서는 ClusterItem 을 상속받게 해서 생성한 마커를 ClusterManager에 전달해주면 됩니다.

우선 ClusterItem을 상속받는 CoronaMedClusterItem 클래스를 작성합니다. 마커 모양은 기본값이 아닌 적십자 이미지를 적용하기 위해 _icon 속성을 정의하고, BitmapDescriptor 를 반환하는 getIcon 메소드를 추가합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
data class CoronaMedClusterItem(
    private val _position: LatLng,
    private val _title: String,
    private val _snippet: String,
    private val _icon: BitmapDescriptor,
) :
    ClusterItem {
    override fun getSnippet(): String {
        return _snippet
    }

    override fun getTitle(): String {
        return _title
    }

    override fun getPosition(): LatLng {
        return _position
    }

    fun getIcon(): BitmapDescriptor {
        return _icon
    }
}

# 클러스터 렌더러 구현

마커는 DefaultClusterRenderer 에 의해 렌더링되는데요, 이 때 렌더러가 비트맵 이미지를 반영해서 렌더링하도록 onBeforeClusterItemRendered에서 markerOptions.icon에 이미지를 지정하도록 합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class CoronaMedClusterRenderer(
    context: Context,
    map: GoogleMap,
    clusterManager: ClusterManager<CoronaMedClusterItem>,
) : DefaultClusterRenderer<CoronaMedClusterItem>(context, map, clusterManager) {
    init {
        clusterManager.renderer = this
    }

    override fun onBeforeClusterItemRendered(item: CoronaMedClusterItem, markerOptions: MarkerOptions) {
        markerOptions.icon(item.getIcon())
        markerOptions.visible(true)
    }
}

# 커스텀 마커 생성 및 클러스터매니저 구현

그럼 클러스터매니저를 이용해서 마커를 추가하는 메소드를 작성합니다. getJsonData 로 json 파일을 읽어와 오브젝트화 합니다. 그리고나서 요소를 하나씩 꺼내와 위치좌표, 타입, 아이콘을 가지는 CoronaMedClusterItem 마커를 만들어 클러스터매니저에 전달해주도록 합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private fun addMarkers() {
    val items = getJsonData("data.json")
    for (item in items) {
        val medClusterItem = CoronaMedClusterItem(
            LatLng(item.REFINE_WGS84_LAT.toDouble(), item.REFINE_WGS84_LOGT.toDouble()),
            item.MEDINST_NM,
            "타입: ${item.DISTRCT_DIV_DTLS}",
            BitmapDescriptorFactory.fromBitmap(mapIcon)
        )
        clusterManager.addItem(medClusterItem)
    }
}

나머지는 OnMapReady()에서 클러스터매니저를 초기화하고 addMarkers를 실행시켜주면 되겠죠.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
+ private lateinit var clusterManager: ClusterManager<CoronaMedClusterItem>

@SuppressLint("MissingPermission")
override fun onMapReady(googleMap: GoogleMap) {
    ...
    
+    clusterManager = ClusterManager(requireContext(), map)
+    CoronaMedClusterRenderer(requireContext(), map, clusterManager)
+    map.setOnCameraIdleListener(clusterManager)

+    addMarkers()
}

이렇게해서 구글맵에 커스텀 마커와 클러스터아이템을 적용하는 법에 대해 알아보았습니다.

Licensed under CC BY 4.0
Built with Hugo
Theme Stack designed by Jimmy