最近的项目需求中需要做聚合功能,研究了一下官方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
}
}
}
版权声明:本文为sfw673306004原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。