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`
  }
}
相关推荐
Amumu121388 分钟前
Js:正则表达式(一)
开发语言·javascript·正则表达式
小江的记录本18 分钟前
【Linux】《Linux常用命令汇总表》
linux·运维·服务器·前端·windows·后端·macos
无人机9011 小时前
Delphi 网络编程实战:TIdTCPClient 与 TIdTCPServer 类深度解析
java·开发语言·前端
lUie INGA2 小时前
rust web框架actix和axum比较
前端·人工智能·rust
OPHKVPS2 小时前
VoidStealer新型窃密攻击:首例利用硬件断点绕过Chrome ABE防护,精准窃取v20_master_key
前端·chrome
月光宝盒造梦师2 小时前
Ant Design Ellipsis 中的判断逻辑 isEleEllipsis 方法非常消耗性能
javascript·react·优化
gechunlian883 小时前
SpringBoot3+Springdoc:v3api-docs可以访问,html无法访问的解决方法
前端·html
驾驭人生3 小时前
ASP.NET Core 实现 SSE 服务器推送|生产级实战教程(含跨域 / Nginx / 前端完整代码)
服务器·前端·nginx
酉鬼女又兒3 小时前
零基础快速入门前端ES6 核心特性详解:Set 数据结构与对象增强写法(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·javascript·职场和发展·蓝桥杯·es6
慧一居士3 小时前
Vue项目中,子组件调用父组件方法示例,以及如何传值示例,对比使用插槽和不使用插槽区别
前端·vue.js