Android 高德地图 点聚合效果[Kotlin&Compose实现]

背景

高德地图为我们提供了点聚合效果的例子:

Demo源码:github.com/amap-demo/a...

由java编写,项目以kotlin作为主要语言,结合当下需求(数据量<=200), 对demo进行了kotlin迁移和必要的精简。

代码

总共3个类

1. Cluster 模型类

kotlin 复制代码
import com.amap.api.maps.model.LatLng
import com.amap.api.maps.model.LatLngBounds

class Cluster internal constructor(val centerLatLng: LatLng) {
    private val items: MutableList<Item> = mutableListOf()

    fun add(item: Item) {
        items.add(item)
    }

    val clusterCount: Int
        get() = items.size

    fun getLatLngBounds(): LatLngBounds {
        val builder = LatLngBounds.Builder()
        for (clusterItem in items.toList()) {
            builder.include(clusterItem.position)
        }
        return builder.build()
    }

    class Item(
        val position: LatLng,
    )
}

2. MarkerBitmapPool 提供marker图标

kotlin 复制代码
import android.content.Context
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.util.LruCache
import android.util.TypedValue
import android.view.Gravity
import android.widget.TextView
import com.amap.api.maps.model.BitmapDescriptor
import com.amap.api.maps.model.BitmapDescriptorFactory

class MarkerBitmapPool(
    private val context: Context,
) {
    private val lruCache: LruCache<String, BitmapDescriptor?> = LruCache<String, BitmapDescriptor?>(40)

    fun get(cluster: Cluster): BitmapDescriptor? {
        val cacheKey = cluster.clusterCount.toString()
        val cache = lruCache.get(cacheKey)
        if (cache != null) {
            return cache
        }
        return create(cluster).also {
            lruCache.put(cacheKey, it)
        }
    }

    private fun create(cluster: Cluster): BitmapDescriptor? {
        val textView = TextView(context).apply {
            gravity = Gravity.CENTER
            setTextColor(Color.BLACK)
            setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f)

            val count = cluster.clusterCount
            if (count > 1) {
                text = count.toString()
                background = getClusterBackgroundDrawable()
            } else {
                setBackgroundResource(android.R.drawable.ic_menu_myplaces)
            }
        }
        return BitmapDescriptorFactory.fromView(textView)
    }

    private fun getClusterBackgroundDrawable(): Drawable {
        val radius = 80.dpToPx.toInt()
        val bitmap = Bitmap.createBitmap(radius * 2, radius * 2, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        val rectF = RectF(0f, 0f, (radius * 2).toFloat(), (radius * 2).toFloat())
        val paint = Paint().apply {
            color = Color.argb(159, 210, 154, 6)
        }
        canvas.drawArc(rectF, 0f, 360f, true, paint)

        return BitmapDrawable(null, bitmap)
    }

    private val Int.dpToPx: Float
        get() = this * Resources.getSystem().displayMetrics.density

    fun clear() {
        lruCache.evictAll()
    }
}

3. ClusterOverlay 渲染marker,聚合逻辑

kotlin 复制代码
import com.amap.api.maps.AMap
import com.amap.api.maps.AMapUtils
import com.amap.api.maps.CameraUpdateFactory.newLatLngBounds
import com.amap.api.maps.TextureMapView
import com.amap.api.maps.model.BitmapDescriptor
import com.amap.api.maps.model.CameraPosition
import com.amap.api.maps.model.LatLng
import com.amap.api.maps.model.LatLngBounds
import com.amap.api.maps.model.Marker
import com.amap.api.maps.model.MarkerOptions
import com.amap.api.maps.model.animation.AlphaAnimation
import com.amap.api.maps.model.animation.Animation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class ClusterOverlay(
    private val aMap: AMap,
    private val markerBitmapProvider: (Cluster) -> BitmapDescriptor?,
    private val onClusterClick: (marker: Marker, Cluster) -> Unit,
    private val shouldAddPointToCluster: (point: LatLng, clusterCenter: LatLng) -> Boolean,
) : AMap.OnCameraChangeListener {
    private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
    private val addedMarkers: MutableList<Marker> = mutableListOf()
    private val addAnimation = AlphaAnimation(0f, 1f)

    private val clusterItems: MutableStateFlow<List<Cluster.Item>> = MutableStateFlow(listOf())
    private val visibleBoundsState: MutableStateFlow<LatLngBounds?> = MutableStateFlow(
        aMap.projection.visibleRegion.latLngBounds,
    )

    @OptIn(ExperimentalCoroutinesApi::class)
    private val visibleBoundsClusters: Flow<List<Cluster>> = combine(clusterItems, visibleBoundsState, ::Pair)
        .mapLatest(::filterVisibleClusterItems)
        .mapLatest(::generateClusters)

    init {
        aMap.setOnCameraChangeListener(this)
        aMap.setOnMarkerClickListener(::onMarkerClick)
        coroutineScope.launch {
            visibleBoundsClusters.collect {
                setClusterMarkers(it)
            }
        }
    }

    fun setClusterItems(items: List<Cluster.Item>) {
        clusterItems.update { items }
    }

    override fun onCameraChange(cameraPosition: CameraPosition) = Unit

    override fun onCameraChangeFinish(cameraPosition: CameraPosition) {
        visibleBoundsState.update { aMap.projection.visibleRegion.latLngBounds }
    }

    private fun onMarkerClick(marker: Marker): Boolean {
        val cluster = marker.getObject() as? Cluster
        if (cluster != null) {
            onClusterClick.invoke(marker, cluster)
            return true
        }
        return false
    }

    private fun setClusterMarkers(clusters: List<Cluster>) {
        val alphaAnimation = AlphaAnimation(1f, 0f)
        for (marker in addedMarkers.toList()) {
            marker.setAnimation(alphaAnimation)
            marker.setAnimationListener(MarkerRemoveAnimationListener(marker))
            marker.startAnimation()
        }
        addedMarkers.clear()
        clusters.forEach(::addClusterMarker)
    }

    private fun addClusterMarker(cluster: Cluster) {
        val markerOptions = MarkerOptions()
            .anchor(0.5f, 0.5f)
            .icon(markerBitmapProvider(cluster))
            .position(cluster.centerLatLng)
        val marker = aMap.addMarker(markerOptions).apply {
            setAnimation(addAnimation)
            setObject(cluster)
            startAnimation()
        }
        addedMarkers.add(marker)
    }

    private suspend fun filterVisibleClusterItems(pair: Pair<List<Cluster.Item>, LatLngBounds?>): List<Cluster.Item> =
        withContext(Dispatchers.IO) {
            val visibleBounds = pair.second ?: return@withContext listOf()
            val clusterItems = pair.first
            val visibleClusterItems = clusterItems.filter { isActive && visibleBounds.contains(it.position) }
            visibleClusterItems
        }

    private suspend fun generateClusters(clusterItems: List<Cluster.Item>): List<Cluster> =
        withContext(Dispatchers.IO) {
            val clusters = mutableListOf<Cluster>()
            clusterItems.forEach { clusterItem ->
                if (isActive) {
                    val find = clusters.find { cluster ->
                        shouldAddPointToCluster(clusterItem.position, cluster.centerLatLng)
                    }
                    if (find != null) {
                        find.add(clusterItem)
                    } else {
                        val newCluster = Cluster(clusterItem.position)
                        newCluster.add(clusterItem)
                        clusters.add(newCluster)
                    }
                }
            }
            clusters
        }

    fun onDestroy() {
        clusterItems.update { listOf() }
        addedMarkers.forEach { it.remove() }
        addedMarkers.clear()
        coroutineScope.cancel()
    }

    private class MarkerRemoveAnimationListener(private val marker: Marker) : Animation.AnimationListener {
        override fun onAnimationStart() = Unit
        override fun onAnimationEnd() {
            marker.remove()
        }
    }
}

接入代码

kotlin 复制代码
import android.content.res.Resources
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.viewinterop.AndroidView
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.amap.api.maps.AMapUtils
import com.amap.api.maps.CameraUpdateFactory.newLatLngBounds
import com.amap.api.maps.TextureMapView
import com.amap.api.maps.model.LatLng
import kotlinx.coroutines.launch

class SampleMapFragment : Fragment() {
    private var textureMapView: TextureMapView? = null
    private var clusterOverlay: ClusterOverlay? = null

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return ComposeView(requireContext()).apply {
            setContent {
                AndroidView(
                    factory = { context ->
                        TextureMapView(context).also { mapView ->
                            textureMapView = mapView
                            mapView.onCreate(savedInstanceState)
                            mockData()
                        }
                    },
                    modifier = Modifier.fillMaxSize(),
                )
            }
        }
    }

    private fun mockData() {
        val mapView: TextureMapView = textureMapView ?: return
        mapView.map.setOnMapLoadedListener {
            val markerBitmapPool = MarkerBitmapPool(mapView.context)
            clusterOverlay = ClusterOverlay(
                aMap = mapView.map,
                markerBitmapProvider = markerBitmapPool::get,
                onClusterClick = { _, cluster ->
                    mapView.map.animateCamera(newLatLngBounds(cluster.getLatLngBounds(), 0))
                },
            ) { point, clusterCenter ->
                val aMap = mapView.map
                //  聚合范围的大小(指点像素单位距离内的点会聚合到一个点显示),100dp可以调整
                val clusterRadiusInPixel: Float = 100 * Resources.getSystem().displayMetrics.density
                val clusterRadiusDistance = aMap.scalePerPixel * clusterRadiusInPixel
                val distance = AMapUtils.calculateLineDistance(point, clusterCenter).toDouble()
                distance < clusterRadiusDistance && aMap.cameraPosition.zoom < 11 // 可以调整数值
            }
        }
        viewLifecycleOwner.lifecycleScope.launch {
            val items = List(200) {
                val lat = Math.random() + 39.474923
                val lon = Math.random() + 116.027116
                val latLng = LatLng(lat, lon, false)
                Cluster.Item(latLng)
            }
            clusterOverlay?.setClusterItems(items)
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        textureMapView?.onSaveInstanceState(outState)
    }

    override fun onResume() {
        super.onResume()
        textureMapView?.onResume()
    }

    override fun onPause() {
        super.onPause()
        textureMapView?.onPause()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        textureMapView?.onDestroy()
        textureMapView = null
        clusterOverlay?.onDestroy()
    }
}
相关推荐
愤怒的代码10 小时前
一个使用 AI 开发的 Android Launcher
android
北京自在科技10 小时前
Find Hub迎来重大升级,UWB技术实现厘米级精准定位,离线追踪覆盖更广
android·google findhub
悠哉清闲10 小时前
SoundPool
android
鹏多多10 小时前
flutter-使用url_launcher打开链接/应用/短信/邮件和评分跳转等
android·前端·flutter
2501_9159214310 小时前
iOS 性能分析工具全景解析,构建从底层诊断到真机监控的多层级性能分析体系
android·ios·小程序·https·uni-app·iphone·webview
zhixingheyi_tian10 小时前
TestDFSIO 之 热点分析
android·java·javascript
2501_9159090610 小时前
如何防止 IPA 被反编译,从攻防视角构建一套真正有效的 iOS 成品保护体系
android·macos·ios·小程序·uni-app·cocoa·iphone
触想工业平板电脑一体机10 小时前
【触想智能】工业触控一体机在工业应用中扮演的角色以及其应用场景分析
android·大数据·运维·电脑·智能电视
克喵的水银蛇10 小时前
Flutter 入门实战:从零搭建跨平台 HelloWorld 应用(适配鸿蒙 / 安卓 /iOS)
android·flutter·harmonyos