大家好我是堂主,目前有个需求就是需要再地图的叠加层里面添加类似Tooltip组件的一个功能,目前代码已实现。基于本人懒,可能就大致说下方案。
需求
- 地图的初始化过程和根据业务的个性化地图配置
- 地图根据目标搜索条件搜索出
targets
,然后在地图上渲染Markers
- 地图移动的时候通过经纬度范围搜索范围内
targets
- 实现地图自定义控制层和附加层
- 控制层:地图放大缩小层级、街景模式、卫星模式、正常模式等
- 附加层:悬浮于地图上的DOM层
- 通过
Overlayview
实现地图上展示DOM
级别的Marker
,提供DOM
的交互和操作,并对渲染Marker
做diff
优化 - 通过
Overlayview
实现地图上hovertarget
的时候展示自定义的信息窗口,并适配地图移动的时候,在地图窗口坐标实现tooltip
的自适应展示效果 - 一些其他的地图优化,比如
LCP
(最大渲染内容)、地图过渡效果优化,平移+缩放 - 抽离一些地图业务逻辑定义为自定义hook
为什么不直接使用组件库的Tooltip
tooltip
的原理是通过getBoundingClientRect
获取位置信息,而需求是直接在地图视窗里,地图视窗直接使用tooltip
,并不会跟着地图移动,因此想要跟着地图移动必须是通过地图的自定义叠加层来实现。- 截止到目前Google map的高级标记(可以渲染DOM)仍在beta中,正式版里只能用icon或者文字来实现
Marker
,所以需要通过地图的自定义叠加层来实现展示内容为DOM的Marker
- 截止到目前高德地图应该是直接能支持DOM级的
Marker
,但是没有Tooltip
正常组件库里的ToolTip实现原理
处理trigger和content
trigger
和content
不为父子关系,通过封装Protal
组件,将content
挂载body
下和根节点统计,原因是父子组件层级z-index
问题。 Protal
组件原理就是React.protal
计算trigger位置
通Element.getBoundingClientRect
来获取到位置信息,配合Tooltip
设置的placement
来计算出position
。需要考虑的点(或者坑)是trigger
所在的父元素可以滑动的情况,而在地图视窗里,父元素没有滑块,暂时不用考虑。
hover trigger到hover content的过程不隐藏content
设置个'实例'状态Ref
,在trigger onMouseLeave
的时候进行设置,需要注意这里来一个debounce
延时设置这个状态,不然会闪一下。content onMouseEnter
的时候检测状态,如果是从trigger移动过来,则不触发后续逻辑。
resize/trigger滑动的时候重新计算content的位置
监听resize和父元素的scroll,计算到父元素的offsetTop、offsetLeft
等信息。重新设定position
地图上OverlayView层实现Tooltip式的交互
通过继承google map OverlayView
实现一个地图叠加层,渲染传入的JSX
,实现OverlayView
绘制children
,同时实现这个children
在地图窗口上显示的时候有类似tooltip
的效果,这个OverlayView应该具有以下功能:
- 位置有两种模式
- 以position(lat\lng)为中心,增加可选offset偏移量,固定渲染到地图上
- 以position(lat\lng)为可选,增加可选offset偏移量,计算位置和地图视窗的关系,完整展示OverlayView的children,实现tooltip的效果
方案:
使用地图的叠加层,实现叠加层的基类OverlayView,通过传入position,type,content,offset来实现
JS
position: { latitude: string | number; longitude: string | number } // 坐标
autoPlacement?: boolean // 位置的两种模式 是否需要类似tooltip自适应位置
content: ReactNode // 传入JSX
offset?: { x: number; y: number } // 偏移量
- 在您的原型中实现
onAdd()
方法,并将叠加层附加到地图。当地图准备好附加叠加层后,系统将会调用OverlayView.onAdd()
。 - 在您的原型中实现
draw()
方法,并处理对象的视觉显示。首次显示对象时,系统会调用OverlayView.draw()
。 - 您还应实现
onRemove()
方法,以清理您在叠加层中添加的任何元素。
实现渲染:
- 在初始化的时候,生成一个父容器
container
来容纳需要渲染的DOM
,通过content
传入ReactNode节点,来实现JSX渲染成DOM。使用root.render
- 在map需要将叠加层附加到地图的时候,会调用onAdd,onAdd里面按照地图API在叠加层添加容器。这里因为调用了
onAdd
才会去调用draw
,导致一开始渲染的内容在地图上的位置不正确,并且会有闪烁问题,因此先隐藏设置一个timemout
调用draw
方法后设置为visible
。 - 在每一帧调用的时候(
draw
方法),去计算需要显示出来的DOM的宽高以及position离地图边距的距离
JS
class OverlayView extends google.maps.OverlayView {
constructor(props) {
super()
this.containerDiv.style.position = `absolute`
const root = createRoot(this.containerDiv)
root.render(content)
}
/** Called when the popup is added to the map. */
onAdd() {
// OverlayView.preventMapHitsAndGesturesFrom(this.containerDiv)
OverlayView.preventMapHitsFrom(this.containerDiv)
this.getPanes()?.overlayMouseTarget.appendChild(this.containerDiv)
this.containerDiv.style.visibility = 'hidden'
setTimeout(() => {
this.draw()
this.containerDiv.style.visibility = 'visible'
}, 10)
}
/** Called when the popup is removed from the map. */
onRemove() {}
/** Called each frame when the popup needs to draw itself. */
draw(){
if (autoPlacement) {
this.calculateInfoBoxPosition()
} else {
this.calculateMarkerPosition()
}
}
}
实现hover trigger到hover content的过程
- 在
Marker
上onMouseHover
的时候触发显示tooltip
的内容 - 在
onMouseLeave
的时候设置一个防抖函数,300ms
后触发,如果这300ms
内鼠标移入到了tooltip
上,则不需要隐藏tooltip
,否则需要隐藏tooltip
- 在外层设置了一个实例状态
tooltipIsHover
,用useRef
保存,当移入到tooltip
的时候,tooltipIsHover
设置为true
,onMouseLeave
防抖函数触发的时候检测tooltipIsHover
,如果为true
则直接返回,如果为false
则隐藏tooltip
并且调用传入的onMouseLeave
回调。
实现自动计算位置
简易版,因为实在地图视窗里,所以不用getBoundingClientRect
,而使用地图视窗的宽高和坐标信息。
js
// autoPlacement模式下 计算位置
calculateInfoBoxPosition() {
// 获取当前postion下的转换为像素后的坐标,以左上角为(0,0)
const divPosition = this.getProjection()?.fromLatLngToDivPixel(this.position)
const containerDivPosition = this.getProjection()?.fromLatLngToContainerPixel(this.position)
if (!containerDivPosition || !divPosition) return
// 计算position左边是否有足够的空间显示组件
if (containerDivPosition.x >= this.containerDiv.offsetWidth + this.offset.x) {
this.containerDiv.style.left = `${
divPosition.x - this.containerDiv.offsetWidth - this.offset.x
}px`
} else {
// 没有则显示在position右边
this.containerDiv.style.left = `${divPosition.x + this.offset.x}px`
}
// 和width一样,只不过设置height的时候多加了显示在中间的模式
if (containerDivPosition.y >= (this.containerDiv.offsetHeight + this.offset.y) / 2) {
const mapDiv = (this.getMap() as google.maps.Map).getDiv()
if (
mapDiv.offsetHeight - containerDivPosition.y <
(this.containerDiv.offsetHeight + this.offset.x) / 2
) {
this.containerDiv.style.top = `${
divPosition.y - (this.containerDiv.offsetHeight + this.offset.x)
}px`
} else {
this.containerDiv.style.top = `${
divPosition.y - (this.containerDiv.offsetHeight + this.offset.x) / 2
}px`
}
} else {
this.containerDiv.style.top = `${divPosition.y + this.offset.y}px`
}
}