最近的項目需求中需要做聚合功能,研究了一下官方demo,發現官方Demo有以下兩個用起來不太方便的點:
- 需要修改ClusterOverlay才能實現自己的Marker繪製邏輯。(僅聚合簇的繪製開放了接口)。
- 不能批量的動態添加和移除數據,如果要做這個功能的話,還是要修改官方的ClusterOverlay實現。
爲了解決這兩個問題,自己實現了一個聚合工具類。主要邏輯和官方demo的邏輯差不多。
大概的聚合邏輯如下:
- 定義聚合簇的結構,它由錨點和吸附於它的一系列點組成,錨點本身也對應着一個有具體數據的標記物。錨點一定範圍內的點被吸附到這個聚合簇。
- 如果當前不存在任何聚合簇,則被循環到的第一個點作爲第一個聚合簇的錨點。
- 如果已經存在聚合簇,則對於其他點,在需要聚合的縮放級別下,判斷它是否位於聚合簇錨點的範圍閾值(單位爲m)內。這個範圍閾值等於clusterPXSize*map.scalePerPixel,如果它位於這個範圍,則其依附於該聚合簇。
- 對於聚合完成後,依然沒有依附於任何聚合簇的孤立點,不將其繪製爲聚合簇的標記物形式,而是直接繪製爲未聚合狀態下的標記物形式。
這個邏輯還是挺簡單的,官方demo也是這樣實現的。除此之外,官方demo加入了聚合物的BitmapDescriptor緩存和開線程計算等優化步驟。
我自己寫的就沒有單開線程進行計算了。
實現效果如下:
如上圖,圖片中的藍色標記物是無法被聚合的孤立點,黑色標記物是聚合簇。
代碼如下:
@file:Suppress("unused")
package com.bian.cluster
import android.content.Context
import android.graphics.Color
import android.util.LruCache
import android.util.TypedValue
import android.view.Gravity
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.TextView
import com.amap.api.maps.AMap
import com.amap.api.maps.AMapUtils.calculateLineDistance
import com.amap.api.maps.CameraUpdateFactory
import com.amap.api.maps.model.*
import com.amap.api.maps.model.animation.AlphaAnimation
import com.amap.api.maps.model.animation.Animation
import com.bian.cluster.CustomClusterOverlay.ClusterModel
/**
* author fhbianling
* date 2020/6/11 17:26
* 類描述:高德聚合工具類
* 聚合邏輯:
* 1.如果當前不存在任何聚合錨點,則被循環到的第一個點作爲聚合錨點
* 2.如果已經存在聚合簇,則對於其他點,在任意map的zoom情況下,判斷它是否位於聚合簇錨點的範圍閾值(單位爲m)內。
* 這個範圍閾值等於clusterPXSize*map.scalePerPixel,如果它位於這個範圍,則其依附於該聚合簇
* 3.對於聚合完成後,依然沒有依附於任何聚合簇的孤立點,不將其繪製爲聚合簇的標記物形式,而是直接繪製爲未聚合狀態下的標記物形式
*
* [ClusterModel]聚合錨點
* [CustomClusterOverlay.clusterRender] 聚合簇標記物渲染器
* [CustomClusterOverlay.markerRender] 未聚合狀態下標記物的渲染器
* [CustomClusterOverlay.clusterPXSize]聚合判斷的屏幕像素範圍
* [CustomClusterOverlay.clusterDisappearZoom]當map.zoom大於或等於該值時,不再進行聚合
*/
class CustomClusterOverlay<T : CustomClusterOverlay.Model>(
private val map: AMap,
private val clusterPXSize: Int,
private val context: Context,
private val zMarkerIndex: Float,
private val zClusterIndex: Float,
private var markerRender: MarkerRender<T>? = null,
private var clusterRender: ClusterRender? = null,
private var clusterDisappearZoom: Int = CLUSTER_DISAPPEAR_ZOOM
) {
private val data = mutableListOf<T>()
private val showingMarkers = mutableMapOf<T, Marker>()
private val clusterMarkers = mutableMapOf<ClusterModel<T>, Marker>()
private val defaultMarkerRender by lazy { DefaultMarkerRender<T>() }
private val defaultClusterRender by lazy { DefaultClusterRender(context) }
private val bdCache = LruCache<Int, BitmapDescriptor>(CLUSTER_BITMAP_DESCRIPTION_CACHE_SIZE)
private val mOnCameraChangeListener = object : AMap.OnCameraChangeListener {
override fun onCameraChangeFinish(p0: CameraPosition?) {
// 鏡頭移動結束後更新聚合物
updateCluster()
}
override fun onCameraChange(p0: CameraPosition?) {
}
}
init {
map.addOnCameraChangeListener(mOnCameraChangeListener)
// 下面的onMarkerClick是實現點擊聚合簇或孤立點時的鏡頭交互
map.addOnMarkerClickListener { marker ->
if (!showingMarkers.values.contains(marker) && !clusterMarkers.values.contains(marker))
return@addOnMarkerClickListener false
val obj = marker.`object` ?: return@addOnMarkerClickListener false
if (obj is ClusterModel<*>) {
if (obj.count == 1) {
map.animateCamera(
CameraUpdateFactory.newLatLngZoom(
obj.model.getPosition(),
clusterDisappearZoom.toFloat()
)
)
return@addOnMarkerClickListener true
} else {
map.animateCamera(CameraUpdateFactory.newLatLngBounds(obj.latLngBounds, 50))
return@addOnMarkerClickListener true
}
} else if (obj is Model) {
map.animateCamera(
CameraUpdateFactory.newLatLngZoom(
obj.getPosition(),
clusterDisappearZoom.toFloat()
)
)
return@addOnMarkerClickListener true
}
return@addOnMarkerClickListener false
}
}
fun setModels(modelList: List<T>, forceClear: Boolean = false) {
if (forceClear) {
removeAll()
addModels(modelList)
} else {
val intersect = data intersect modelList
val newMinusOld = modelList.toMutableList().also { it.removeAll(intersect) }
val oldMinusNew = data.also { it.removeAll(intersect) }
addModels(newMinusOld, false)
removeModels(oldMinusNew)
}
}
fun addModels(modelList: List<T>, update: Boolean = true) {
data.addAll(modelList)
if (update) {
updateCluster()
}
}
fun addModel(model: T) {
data.add(model)
updateCluster()
}
fun removeModels(modelList: List<T>) {
val toSet = modelList.toSet()
val toRemoveShowingMarker = showingMarkers.filter { toSet.contains(it.key) }
toRemoveShowingMarker.forEach {
it.value.remove()
showingMarkers.remove(it.key)
}
val toRemoveClusterMarker = clusterMarkers.filter { toSet.contains(it.key.model) }
toRemoveClusterMarker.forEach {
it.value.remove()
clusterMarkers.remove(it.key)
}
data.removeAll(modelList)
updateCluster()
}
fun removeModel(model: T) {
data.remove(model)
showingMarkers[model]?.remove()
showingMarkers.remove(model)
val toRemove = clusterMarkers.filter { model == it.key.model }
toRemove.forEach {
it.value.remove()
clusterMarkers.remove(it.key)
}
updateCluster()
}
fun removeAll() {
data.clear()
showingMarkers.values.forEach { it.remove() }
clusterMarkers.values.forEach { it.remove() }
showingMarkers.clear()
clusterMarkers.clear()
}
// 更新聚合簇、孤立點標記或未聚合狀態的標記
private fun updateCluster() {
val markerRender = this.markerRender ?: defaultMarkerRender
val clusterRender = this.clusterRender ?: defaultClusterRender
val zoom = map.cameraPosition.zoom
// 根據zoom判斷當前是否顯示聚合簇
val showCluster = zoom < clusterDisappearZoom
val visibleBounds: LatLngBounds = map.projection.visibleRegion.latLngBounds
// 在聚合情況下,會出現某些孤立點無法被依附到任何聚合簇上的情況
// 直接畫出這些孤立點未聚合狀態下的marker
val isolatedModel = updateClusterMarkers(showCluster, visibleBounds, clusterRender)
updateDataMarkers(showCluster, visibleBounds, markerRender, isolatedModel)
}
private fun updateClusterMarkers(
showCluster: Boolean,
visibleBounds: LatLngBounds,
clusterRender: ClusterRender
): List<T>? {
if (!showCluster) {
clusterMarkers.forEach { it.value.remove() }
clusterMarkers.clear()
return null
}
val scalePerPixel = map.scalePerPixel
val onScreenModels = data.filter { visibleBounds.contains(it.getPosition()) }
val clusters = mutableListOf<ClusterModel<T>>()
val isolatedModels = mutableListOf<T>()
onScreenModels.forEach { model ->
if (model.isClusterEnable()) {
var clusterModel = getAdsorptionClusterModel(model, scalePerPixel, clusters)
if (clusterModel == null) {
clusterModel = ClusterModel(model)
clusters.add(clusterModel)
} else {
clusterModel.addSubModel(model)
}
} else {
isolatedModels.add(model)
}
}
val toRemove =
clusterMarkers.filter { !clusters.contains(it.key) }
val anim = AlphaAnimation(1f, 0f)
toRemove.forEach {
val marker = it.value
marker.setAnimation(anim)
marker.setAnimationListener(MyRemoveMarkerAnimationListener(marker))
marker.startAnimation()
clusterMarkers.remove(it.key)
}
clusters.forEach { clusterModel ->
if (clusterModel.count == 1) {
isolatedModels.add(clusterModel.model)
return@forEach
}
val bd = bdCache[clusterModel.count]
?: clusterRender.createBitmapDescriptor(clusterModel.count)
bdCache.put(clusterModel.count, bd)
if (this.clusterMarkers.containsKey(clusterModel)) {
clusterMarkers[clusterModel]?.setIcon(bd)
} else {
this.clusterMarkers[clusterModel] =
map.addMarker(
MarkerOptions().position(clusterModel.model.getPosition()).icon(bd)
).also {
it.`object` = clusterModel
it.zIndex = zClusterIndex
}
}
}
return isolatedModels
}
private fun updateDataMarkers(
showCluster: Boolean,
visibleBounds: LatLngBounds,
markerRender: MarkerRender<T>,
isolatedModel: List<T>?
) {
if (showCluster) {
showingMarkers.forEach { it.value.remove() }
showingMarkers.clear()
// 繪製孤立點
isolatedModel?.forEach { model ->
if (visibleBounds.contains(model.getPosition())) {
val marker = markerRender.createMarker(model, map, true)
.also {
it.`object` = model
it.zIndex = zClusterIndex
}
showingMarkers[model] = marker
}
}
return
}
val toRemoveShowingMarkers =
showingMarkers.filter { !visibleBounds.contains(it.key.getPosition()) }
toRemoveShowingMarkers.forEach {
it.value.remove()
showingMarkers.remove(it.key)
}
data.forEach {
if (visibleBounds.contains(it.getPosition())) {
if (showingMarkers.containsKey(it)) {
showingMarkers[it]?.remove()
}
val marker = markerRender.createMarker(it, map, false)
marker.zIndex = zMarkerIndex
showingMarkers[it] = marker
}
}
}
/**
* 找到一個可以被model吸附的聚合簇
* @param model 等待被聚合的數據
* @param scalePerPixel 當前縮放級別下,地圖上1像素點對應的長度,單位米。
* @param clusterModels 已經存在的聚合簇
*/
private fun getAdsorptionClusterModel(
model: T,
scalePerPixel: Float,
clusterModels: MutableList<ClusterModel<T>>
): ClusterModel<T>? {
return clusterModels.firstOrNull { it.contains(model, scalePerPixel, clusterPXSize) }
}
companion object {
// 聚合簇消失的層級
private const val CLUSTER_DISAPPEAR_ZOOM = 19
// LRUCache緩存聚合簇BitmapDescription的數量上限
private const val CLUSTER_BITMAP_DESCRIPTION_CACHE_SIZE = 80
}
/**
* 數據模型接口,實現該接口後,
* 通過[MarkerRender]接口可以對數據進行自定義的Marker繪製
*/
interface Model {
fun getPosition(): LatLng
/**
* 可能存在某些數據不需要被聚合的情況,如果該方法返回false,
* 則這一個數據不會被納入聚合計算中,它的表現始終同孤立點一樣
*/
fun isClusterEnable(): Boolean
}
/**
* 聚合簇繪製接口
*/
interface ClusterRender {
/**
* @param clusterCount 被聚合到該聚合簇的數據數量
*/
fun createBitmapDescriptor(clusterCount: Int): BitmapDescriptor
}
/**
* 標記繪製接口
*/
interface MarkerRender<T : Model> {
/**
* 該接口方法在以下兩種情況下被調用:
* 1.當前zoom大於等於[clusterDisappearZoom],此時不進行聚合,所有數據正常顯示爲標記
* 2.當前zoom小於[clusterDisappearZoom],某些點爲聚合後仍然孤立的單點,
* 某些點對應的數據[Model.isClusterEnable]返回爲false,這兩類點仍然以
* 未聚合標記物的形式被繪製
*/
fun createMarker(model: T, map: AMap, isolated: Boolean): Marker
}
private class MyRemoveMarkerAnimationListener(private val marker: Marker) :
Animation.AnimationListener {
override fun onAnimationEnd() {
marker.remove()
}
override fun onAnimationStart() {
}
}
private class DefaultClusterRender(private val ctx: Context) : ClusterRender {
override fun createBitmapDescriptor(clusterCount: Int): BitmapDescriptor {
val textView = TextView(ctx)
textView.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
textView.gravity = Gravity.CENTER
textView.setTextColor(Color.WHITE)
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f)
textView.setBackgroundColor(Color.BLUE)
textView.text = clusterCount.toString()
return BitmapDescriptorFactory.fromView(textView)
}
}
private class DefaultMarkerRender<T : Model> : MarkerRender<T> {
override fun createMarker(model: T, map: AMap, isolated: Boolean): Marker {
return map.addMarker(MarkerOptions().position(model.getPosition()))
.also { it.title = it.id }
}
}
/**
* 記錄聚合簇信息的私有類
*/
private class ClusterModel<T : Model>(val model: T) {
private val mSubModels = mutableListOf<T>()
val count: Int
get() = (mSubModels.size + 1)// 加上自己
val latLngBounds: LatLngBounds
get() {
val builder = LatLngBounds.Builder()
mSubModels.forEach {
builder.include(it.getPosition())
}
builder.include(model.getPosition())
return builder.build()
}
fun contains(model: T, scale: Float, clusterPXSize: Int): Boolean {
val distance =
calculateLineDistance(this.model.getPosition(), model.getPosition())
return distance < scale * clusterPXSize
}
fun addSubModel(sub: T) {
mSubModels.add(sub)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ClusterModel<*>
if (model != other.model) return false
if (mSubModels != other.mSubModels) return false
return true
}
override fun hashCode(): Int {
var result = model.hashCode()
result = 31 * result + mSubModels.hashCode()
return result
}
}
}