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
        }


    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章