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`
  }
}
相关推荐
行走的陀螺仪8 分钟前
vue3-封装权限按钮组件和自定义指令
前端·vue3·js·自定义指令·权限按钮
isyuah17 分钟前
vite-plugin-openapi-ts CLI 使用指南
前端·vite
qq_3985865430 分钟前
浏览器中内嵌一个浏览器
前端·javascript·css·css3
Mapmost1 小时前
地图引擎性能优化:解决3DTiles加载痛点的六大核心策略
前端
San30.1 小时前
Ajax 数据请求:从 XMLHttpRequest 到现代前端数据交互的演进
前端·ajax·交互
西西西西胡萝卜鸡1 小时前
虚拟列表(Virtual List)组件实现与优化铁臂猿版(简易版)
前端·vue.js
宇余2 小时前
Unibest:新一代uni-app工程化最佳实践指南
前端·vue.js
*小雪2 小时前
uniapp写H5授权登录及分享,返回到目标页面
开发语言·javascript·uni-app
性野喜悲2 小时前
ts+uniapp小程序时间日期选择框(分开选择)
前端·javascript·vue.js
你不是我我2 小时前
【Java 开发日记】SQL 语句左连接右连接内连接如何使用,区别是什么?
java·javascript·数据库