GoogleMap/高德Map 实现跟随式Tooltip

大家好我是堂主,目前有个需求就是需要再地图的叠加层里面添加类似Tooltip组件的一个功能,目前代码已实现。基于本人懒,可能就大致说下方案。

需求

  • 地图的初始化过程和根据业务的个性化地图配置
  • 地图根据目标搜索条件搜索出targets,然后在地图上渲染Markers
  • 地图移动的时候通过经纬度范围搜索范围内targets
  • 实现地图自定义控制层和附加层
    • 控制层:地图放大缩小层级、街景模式、卫星模式、正常模式等
    • 附加层:悬浮于地图上的DOM层
  • 通过Overlayview实现地图上展示DOM级别的Marker,提供DOM的交互和操作,并对渲染Markerdiff优化
  • 通过Overlayview实现地图上hovertarget的时候展示自定义的信息窗口,并适配地图移动的时候,在地图窗口坐标实现tooltip的自适应展示效果
  • 一些其他的地图优化,比如LCP(最大渲染内容)、地图过渡效果优化,平移+缩放
  • 抽离一些地图业务逻辑定义为自定义hook

为什么不直接使用组件库的Tooltip

  • tooltip的原理是通过getBoundingClientRect获取位置信息,而需求是直接在地图视窗里,地图视窗直接使用tooltip,并不会跟着地图移动,因此想要跟着地图移动必须是通过地图的自定义叠加层来实现。
  • 截止到目前Google map的高级标记(可以渲染DOM)仍在beta中,正式版里只能用icon或者文字来实现Marker,所以需要通过地图的自定义叠加层来实现展示内容为DOM的Marker
  • 截止到目前高德地图应该是直接能支持DOM级的Marker,但是没有Tooltip

正常组件库里的ToolTip实现原理

处理trigger和content

triggercontent不为父子关系,通过封装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的过程

  • MarkeronMouseHover的时候触发显示tooltip的内容
  • onMouseLeave的时候设置一个防抖函数,300ms后触发,如果这300ms内鼠标移入到了tooltip上,则不需要隐藏tooltip,否则需要隐藏tooltip
  • 在外层设置了一个实例状态tooltipIsHover,用useRef保存,当移入到tooltip的时候,tooltipIsHover设置为trueonMouseLeave防抖函数触发的时候检测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`
  }
}
相关推荐
Leyla2 分钟前
【代码重构】好的重构与坏的重构
前端
影子落人间6 分钟前
已解决npm ERR! request to https://registry.npm.taobao.org/@vant%2farea-data failed
前端·npm·node.js
世俗ˊ30 分钟前
CSS入门笔记
前端·css·笔记
子非鱼92130 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
6230_35 分钟前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人44 分钟前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
加勒比海涛1 小时前
HTML 揭秘:HTML 编码快速入门
前端·html
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
茶茶只知道学习1 小时前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css