You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在@react-google-maps/api中实现自定义OverlayView标记的聚类?

解决@react-google-maps/api中自定义标记与聚类共存的问题

核心问题

OverlayView无法配合MarkerClusterer实现聚类,因为MarkerClusterer仅识别带有clusterer属性的Marker组件,而OverlayView不具备该属性。下面提供两种可行方案:


方案一:自定义Marker组件(推荐,原生支持聚类)

直接基于Marker组件自定义样式,既满足个性化展示需求,又能正常参与聚类。

实现步骤

  1. 构造自定义Marker图标:利用Marker的icon属性,将用户头像+边框样式转换为Google Maps支持的图标格式(DataURL/SVG/图片URL均可)。
  2. 绑定聚类属性:在MarkerClusterer的回调中,将clusterer参数传递给每个Marker的clusterer属性。
  3. 添加交互效果:通过Marker的事件监听(onMouseOver/onMouseOut)实现悬停状态切换。

修改后的代码示例

import React, { useEffect, useState, useRef } from 'react';
import { GoogleMap, useJsApiLoader, Marker, MarkerClusterer } from '@react-google-maps/api';
import './app.css';
import PlacesAutocomplete from './components/PlacesAutocomplete';

const containerStyle = {
    width: '100vw',
    height: '100vh',
};

// 生成带边框的头像图标(SVG转DataURL)
const buildCustomMarkerIcon = (avatarUrl: string, borderColor: string = '#2196F3', size: number = 40) => {
    const radius = size / 2;
    const innerRadius = radius - 2;
    const svg = `
        <svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
            <circle cx="${radius}" cy="${radius}" r="${radius - 1}" fill="${borderColor}" />
            <circle cx="${radius}" cy="${radius}" r="${innerRadius}" fill="white" />
            <image href="${avatarUrl}" x="${radius - innerRadius}" y="${radius - innerRadius}" width="${innerRadius * 2}" height="${innerRadius * 2}" rx="${innerRadius}" ry="${innerRadius}" />
        </svg>
    `;
    return `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svg)))}`;
};

const App = () => {
    const [center, setCenter] = useState({ lat: 39.015137, lng: 34.97953 });
    const [zoom, setZoom] = useState(6);
    const [markers, setMarkers] = useState<{ id: string; name: string; avatar: string; latLng?: google.maps.LatLng }[]>([]);
    const [hoveredMarkerId, setHoveredMarkerId] = useState<string | null>(null);
    const mapRef = useRef<google.maps.Map>(null);

    const { isLoaded } = useJsApiLoader({
        id: 'google-map-script',
        googleMapsApiKey: 'API_KEY',
        libraries: ['places'],
    });

    const handleAddMarker = (e: google.maps.MapMouseEvent) => {
        if (e.latLng) {
            setMarkers(prev => [...prev, {
                id: Date.now().toString(),
                name: 'Test User',
                avatar: 'https://example.com/avatar.jpg',
                latLng: e.latLng
            }]);
        }
    };

    return isLoaded ? (
        <>
            <PlacesAutocomplete setCenter={setCenter} setZoom={setZoom} />
            <GoogleMap 
                ref={mapRef} 
                mapContainerStyle={containerStyle} 
                center={center} 
                zoom={zoom} 
                onClick={handleAddMarker}
            >
                <MarkerClusterer
                    styles={['blue', 'green', 'red'].map(color => ({
                        url: `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(`
                            <svg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg">
                                <circle cx="30" cy="30" r="28" fill="${color}" />
                                <text x="30" y="35" text-anchor="middle" font-size="18" fill="white">%d</text>
                            </svg>
                        `)))}`,
                        height: 60,
                        width: 60,
                        textColor: 'white',
                        textSize: 15,
                    }))}
                    zoomOnClick={true}
                >
                    {(clusterer) => (
                        <>
                            {markers?.map((obj) => {
                                const isHovered = hoveredMarkerId === obj.id;
                                return (
                                    <Marker
                                        key={obj.id}
                                        position={obj.latLng!}
                                        clusterer={clusterer}
                                        icon={{
                                            url: buildCustomMarkerIcon(obj.avatar, isHovered ? '#FF5722' : '#2196F3', isHovered ? 44 : 40),
                                            scaledSize: new window.google.maps.Size(isHovered ? 44 : 40, isHovered ? 44 : 40),
                                            anchor: new window.google.maps.Point(isHovered ? 22 : 20, isHovered ? 22 : 20)
                                        }}
                                        onMouseOver={() => setHoveredMarkerId(obj.id)}
                                        onMouseOut={() => setHoveredMarkerId(null)}
                                    />
                                )
                            })}
                        </>
                    )}
                </MarkerClusterer>
            </GoogleMap>
        </>
    ) : (
        <div>Loading...</div>
    );
};

export default React.memo(App);

关键说明

  • buildCustomMarkerIcon:动态生成带边框的头像SVG,支持自定义边框颜色和图标尺寸,转成DataURL后可直接作为Marker图标。
  • clusterer属性:必须传递给每个Marker,这是MarkerClusterer识别标记并执行聚类的核心。
  • 悬停效果:通过切换icon的尺寸和边框颜色实现,使用单个状态变量追踪当前悬停的标记ID,避免全局状态冲突。

方案二:手动实现OverlayView聚类(不推荐,复杂度高)

如果必须使用OverlayView,需要自行实现聚类逻辑:

  • 监听地图缩放事件,实时计算所有OverlayView标记的地理距离。
  • 当标记距离小于设定阈值时,隐藏原始标记,创建自定义聚类标记(用Marker或OverlayView)。
  • 处理聚类标记的点击事件,触发地图放大以显示原始标记。

这种方案需要手动维护聚类状态、标记的显示/隐藏,开发成本高且性能不如原生MarkerClusterer,仅适合特殊场景使用。


内容的提问来源于stack exchange,提问作者Kerem İlhan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 18:43:12