android 高德聚合实现

最近的项目需求中需要做聚合功能,研究了一下官方demo,发现官方Demo有以下两个用起来不太方便的点:

  1. 需要修改ClusterOverlay才能实现自己的Marker绘制逻辑。(仅聚合簇的绘制开放了接口)。
  2. 不能批量的动态添加和移除数据,如果要做这个功能的话,还是要修改官方的ClusterOverlay实现。

为了解决这两个问题,自己实现了一个聚合工具类。主要逻辑和官方demo的逻辑差不多。

大概的聚合逻辑如下:

  1. 定义聚合簇的结构,它由锚点和吸附于它的一系列点组成,锚点本身也对应着一个有具体数据的标记物。锚点一定范围内的点被吸附到这个聚合簇。
  2. 如果当前不存在任何聚合簇,则被循环到的第一个点作为第一个聚合簇的锚点。
  3. 如果已经存在聚合簇,则对于其他点,在需要聚合的缩放级别下,判断它是否位于聚合簇锚点的范围阈值(单位为m)内。这个范围阈值等于clusterPXSize*map.scalePerPixel,如果它位于这个范围,则其依附于该聚合簇。
  4. 对于聚合完成后,依然没有依附于任何聚合簇的孤立点,不将其绘制为聚合簇的标记物形式,而是直接绘制为未聚合状态下的标记物形式。

这个逻辑还是挺简单的,官方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版权协议,转载请附上原文出处链接和本声明。