일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- NavController
- Compose
- UiState
- 코틀린
- XML
- 안드로이드
- Jetpack
- DiffUtil
- NavHost
- 플레이스토어
- Android
- Authentication
- cleanarchitecture
- 커스텀뷰
- Flow
- Kotlin
- 파이어베이스
- Build variants
- ListAdapter
- MVVM
- 뷰
- 리사이클러뷰
- coroutine
- 컴포즈
- 로그인
- 회원가입
- 클린아키텍처
- 코딩테스트
- 알고리즘
- sharedFlow
- Today
- Total
Grusie 안드로이드 개발 기술 블로그
[Android] 네이버 맵 공식 클러스터링 적용법 본문
네이버 클라우드에서 제공하는 네이버맵 SDK를 사용했던 적이 몇 번 있었다.
회사 프로젝트에서도 사용했었는데, 클러스터링을 적용하려고 보니, 지원을 하지 않아서 서드파티 라이브러리를 사용했던 기억이 있다.
웹에서는 지원을 하는데, 앱에서는 지원을 안 해줘서 너무하다는 생각을 했었다.
그러던 중 올해초에 클러스터링이 업데이트 되었다는 것을 보았고, 네이버 맵을 적용하게 될 기회가 또 생겨서 기록해보려고 한다.
우선 네이버맵을 화면에 표시하는 방법은 공식문서에 잘 나와 있기에 넘어가도록 하겠다.
바로 커스텀 클러스터링으로 넘어가자
기본 사용법
키 정의
마커 클러스터링을 사용하기 위해서는 ClusteringKey 인터페이스를 구현한 클래스를 정의해야 한다.
class ClusterItemKey(private val travelItemVo: TravelItemVo, private val position: LatLng) :
ClusteringKey {
val id = travelItemVo.id!!
override fun getPosition() = position
fun getTravelItemVo() = travelItemVo
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val itemKey = other as ClusterItemKey
return id == itemKey.id
}
override fun hashCode() = id
}
공식문서에서는 다른 데이터와의 차이를 id와 position만 입력받아서 처리하도록 되어있었다.
문서를 보며 clustering을 사용할 때 걱정이었던 점은 커스텀 VO를 사용할텐데, id와 position만으로는 안되겠다는 생각을 했었다.
그래서 생각한 방법은, VO자체를 넘겨주고, 필요할 때 마커정보에서 꺼내서 쓰면 되겠다는 생각을 하였다.
클러스터링 생성
Clusterer라는 객체를 생성해서 사용하는데, 직접생성은 되지 않아, Builder패턴을 사용한다.
val builder: Clusterer.ComplexBuilder<ClusterItemKey> = Clusterer.ComplexBuilder<ClusterItemKey>()
기본 사용법은 Cluster.Builder<ItemKey>()이나, 추 후 클러스터링 태그를 사용해서 데이터를 관리하기 위해, 복잡한 작업이 가능한 complexBuilder를 사용하게 되었다.
데이터 추가
val clusterer: Clusterer<ClusterItemKey> = builder.build()
var id = 0
val keyTagMap = mutableMapOf<ClusterItemKey, TravelItemVo?>()
repeat(200) {
val travelItemVo = TravelItemVo(
id = id++,
attachs = listOf(Attach(path = "https://cdn.discordapp.com/avatars/429996274104926208/b0a769d357cb9a2b738579b335c6742b.png"))
)
keyTagMap[ClusterItemKey(travelItemVo, getRandomLatLng())] = travelItemVo
}
clusterer.addAll(keyTagMap)
clusterer.map = naverMap
데이터를 추가하는 방법은, Map을 사용하여, ItemKey와, 태그를 넘겨주는 것이라고 한다.
공식문서에서는 태그에 대한 설명이 따로 없고 null로 해둬서 어떤 것을 위해 사용하는 것인지 몰랐지만, 사용하다 보니 클러스터링을 할 때 하위의 마커들의 태그들을 합쳐서 사용할 수 있는 기능이 있었다.(이것을 위해 complexBuilder를 사용함)
태그에는 현재 마커에 들어간 데이터를 넣어두고, 테스트를 위해 랜덤한 위치에 약 200개의 마커를 넣도록 구현하였다.
클러스터링에서 태그 사용
공식문서의 글을 인용해보면
단말 노드에는 Clusterer.add()로 지정한 키와 태그가 유지됩니다. 하지만 클러스터 노드는 여러 노드가 클러스터링되어 만들어진 것이므로 키를 가질 수 없으며, 태그 역시 자동으로 생성할 수 없습니다.
즉 클러스터링에서는 태그를 자동으로 가질 수 없기 때문에 태그 병합 전략을 사용해야 하는데, 그 방법이 builder에서 tagMergeStrategy를 사용하는 것이다. tagMergeStrategy는 complexBuilder에만 존재하는 함수이다.
builder.tagMergeStrategy { cluster ->
cluster.children[0].tag
}
기존에 생성해둔 builder에 tagMergeStrategy를 호출하여, 로직을 구현한다.
필자의 경우, tag에 Vo를 넣어두었고, 클러스터된 아이템 중 랜덤으로 이미지를 띄우기 위해 0번째 tag를 그냥 넘겨주는 것으로 구현하였다.
클러스터링 커스텀 마커
준비가 전부 끝났으면, 클러스터링 마커를 커스텀 해봐야겠다.
사실 마커만 찍는 것이면, 위의 과정들을 생략해도 되는데, 커스텀 마커를 그리기 위해 준비과정이 필요했던 것이다.
val builder: Clusterer.ComplexBuilder<ClusterItemKey> =
Clusterer.ComplexBuilder<ClusterItemKey>().apply {
clusterMarkerUpdater(object : DefaultClusterMarkerUpdater() {
override fun updateClusterMarker(info: ClusterMarkerInfo, marker: Marker) {
super.updateClusterMarker(info, marker)
val clusterItem = (info.tag as TravelItemVo)
Glide.with(requireContext())
.load(clusterItem.attachs?.get(0)?.path)
.circleCrop()
.into(object : CustomTarget<Drawable>() {
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable>?
) {
// 이미지 설정 후 마커 업데이트
clusterBinding.ivMarker.setImageDrawable(resource)
clusterBinding.tvCluster.text = info.size.toString()
marker.icon = OverlayImage.fromView(clusterBinding.root)
}
override fun onLoadCleared(placeholder: Drawable?) {
// 필요 시, 이미지 로드가 취소될 때 처리
}
}
)
}
})
우선 클러스터링의 마커를 변경하기 위해서는, clusterMarkerUpdater의 updateClusterMarker를 재정의하면 되는데,
이 함수는 ClusteringMarkerInfo와, Marker를 파라미터로 받아온다.
ClusteringMarkerInfo에서 tag를 가져와서 활용이 가능하기에, 태그에 Vo를 넣어서 그 아이템의 이미지를 불러오는 형태로 사용하게 되었다.
커스텀 XML뷰를 사용하기 위해, binding을 정의하고, marker의 icon을 OverlayImage.fromView(clusterBinding.root)를 통해 해당하는 XML뷰를 마커로 사용하도록 하였다.
마커를 생성할 때, 이미지가 미리 로드가 되어있지 않으면 제대로 표시되지 않아, Glider의 onResourceReady를 활용해, 이미지가 로드 되었을 때 마커를 넣도록 구현하였다.
커스텀 마커 생성
.leafMarkerUpdater(object : DefaultLeafMarkerUpdater() {
override fun updateLeafMarker(info: LeafMarkerInfo, marker: Marker) {
super.updateLeafMarker(info, marker)
val travelItemVo = (info.key as ClusterItemKey).getTravelItemVo()
Glide.with(requireContext())
.load(travelItemVo.attachs?.get(0)?.path)
.circleCrop()
.into(object : CustomTarget<Drawable>() {
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable>?
) {
// 이미지 설정 후 마커 업데이트
markerBinding.ivMarker.background = resource
marker.icon = OverlayImage.fromView(markerBinding.root)
}
override fun onLoadCleared(placeholder: Drawable?) {
// 필요 시, 이미지 로드가 취소될 때 처리
}
}
)
}
})
클러스터링을 사용하는 마커는, leafMarkerUpdater의 updateLeafMarker를 재정의 하여 수정할 수 있다.
전체 코드
val markerBinding = ItemTravelMapMarkerBinding.inflate(LayoutInflater.from(context))
val clusterBinding = ItemTravelMapClusterBinding.inflate(LayoutInflater.from(context))
val builder: Clusterer.ComplexBuilder<ClusterItemKey> =
Clusterer.ComplexBuilder<ClusterItemKey>().apply {
clusterMarkerUpdater(object : DefaultClusterMarkerUpdater() {
override fun updateClusterMarker(info: ClusterMarkerInfo, marker: Marker) {
super.updateClusterMarker(info, marker)
val clusterItem = (info.tag as TravelItemVo)
Glide.with(requireContext())
.load(clusterItem.attachs?.get(0)?.path)
.circleCrop()
.into(object : CustomTarget<Drawable>() {
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable>?
) {
// 이미지 설정 후 마커 업데이트
clusterBinding.ivMarker.setImageDrawable(resource)
clusterBinding.tvCluster.text = info.size.toString()
marker.icon = OverlayImage.fromView(clusterBinding.root)
Log.d("confirm tag2", "${info.tag}")
}
override fun onLoadCleared(placeholder: Drawable?) {
// 필요 시, 이미지 로드가 취소될 때 처리
}
}
)
}
}).leafMarkerUpdater(object : DefaultLeafMarkerUpdater() {
override fun updateLeafMarker(info: LeafMarkerInfo, marker: Marker) {
super.updateLeafMarker(info, marker)
val travelItemVo = (info.key as ClusterItemKey).getTravelItemVo()
Glide.with(requireContext())
.load(travelItemVo.attachs?.get(0)?.path)
.circleCrop()
.into(object : CustomTarget<Drawable>() {
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable>?
) {
// 이미지 설정 후 마커 업데이트
markerBinding.ivMarker.background = resource
marker.icon = OverlayImage.fromView(markerBinding.root)
Log.d("confirm tag", "${info.tag}")
}
override fun onLoadCleared(placeholder: Drawable?) {
// 필요 시, 이미지 로드가 취소될 때 처리
}
}
)
}
})
}
builder.tagMergeStrategy { cluster ->
cluster.children[0].tag
}
val clusterer: Clusterer<ClusterItemKey> = builder.build()
var id = 0
val keyTagMap = mutableMapOf<ClusterItemKey, TravelItemVo?>()
repeat(200) {
val travelItemVo = TravelItemVo(
id = id++,
attachs = listOf(Attach(path = "https://cdn.discordapp.com/avatars/429996274104926208/b0a769d357cb9a2b738579b335c6742b.png"))
)
keyTagMap[ClusterItemKey(travelItemVo, getRandomLatLng())] = travelItemVo
}
clusterer.addAll(keyTagMap)
clusterer.map = naverMap
결과물
참고
https://navermaps.github.io/android-map-sdk/guide-ko/5-8.html
후기
네이버 문서가 정말 간결하게 잘 나와있긴 하나, 설명이 부족한 부분이 있었던 것 같아 이해하는데 시간이 걸렸다.
특히 태그를 어떻게 사용하라고 있는건지에 대한 설명이 부족해서 시간이 걸렸던 것 같다.
이전에 사용했던 TedCluster라이브러리는 리스트를 넣는 형태로 바로바로 사용이 가능해서 이해하기 쉬웠는데, 조금 복잡했던 것 같다. 그래도 공식으로 지원하는 것이니 만큼 앞으로 사용할 때 도움이 될 것으로 예상된다.
나온지 얼마 되지 않아 인터넷에도 자료가 거의 없다싶이 했어서 작성하게 되었다.
'안드로이드 개발 > 라이브러리' 카테고리의 다른 글
[Android] Glide Permission Denied Exception (0) | 2024.07.30 |
---|---|
[Android] DataStore 사용하기(HashMap형태 저장) (0) | 2024.05.23 |
[Android] Data binding 기본 사용법 및 BindingAdapter (0) | 2024.04.26 |
[Android] 이미지 로딩 라이브러리 Glide, Fresco (Glide를 사용한 이유) (1) | 2024.04.19 |
[Android]RxJava 사용하기 (0) | 2024.04.15 |