源码分析之Leaflet中Map类扩展方法之Drag

发布于:2025-04-17 ⋅ 阅读:(36) ⋅ 点赞:(0)

概述

Drag是 Leaflet 中处理地图拖拽的核心模块,实现了拖拽、惯性滑动、边界限制及世界无缝循环等功能。

源码分析

源码实现

Drag的源码实现如下:

Map.mergeOptions({
  dragging: true, //是否启用拖拽
  inertia: true, // 是否启用惯性滑动
  inertiaDeceleration: 3400, // 惯性减速度 px/s^2
  inertiaMaxSpeed: Infinity, // 最大惯性速度
  easeLinearity: 0.2, // 缓动线性系数
  worldCopyJump: false, // 是否启用世界无缝循环
  maxBoundsViscosity: 0.0, // 边界粘滞系数 (0.1 - 1.0,值越大越难拖出边界)
});

export var Drag = Handler.extend({
  addHooks: function () {
    if (!this._draggable) {
      var map = this._map;
      // 初始化 Draggable 实例
      this._draggable = new Draggable(map._mapPane, map._container);

      // 事件绑定
      this._draggable.on(
        {
          dragstart: this._onDragStart,
          drag: this._onDrag,
          dragend: this._onDragEnd,
        },
        this
      );

      this._draggable.on("predrag", this._onPreDragLimit, this);
      // 世界循环处理
      if (map.options.worldCopyJump) {
        this._draggable.on("predrag", this._onPreDragWrap, this);
        map.on("zoomend", this._onZoomEnd, this);

        map.whenReady(this._onZoomEnd, this);
      }
    }
    // 添加样式类
    DomUtil.addClass(this._map._container, "leaflet-grab leaflet-touch-drag");
    // 启用拖拽
    this._draggable.enable();
    this._positions = [];
    this._times = [];
  },
  removeHooks: function () {
    // 移除样式类,禁用Draggable实例
    DomUtil.removeClass(this._map._container, "leaflet-grab");
    DomUtil.removeClass(this._map._container, "leaflet-touch-drag");
    this._draggable.disable();
  },
  moved: function () {
    return this._draggable && this._draggable._moved;
  },
  moving: function () {
    return this._draggable && this._draggable._moving;
  },
  _onDragStart: function () {
    var map = this._map;
    // 停止当前动画
    map._stop();

    // 边界粘滞计算
    if (this._map.options.maxBounds && this._map.options.maxBoundsViscosity) {
      var bounds = latLngBounds(this._map.options.maxBounds);
      // 将地理边界转换为容器坐标的偏移限制
      this._offsetLimit = toBounds(
        this._map.latLngToContainerPoint(bounds.getNorthWest()).multiplyBy(-1),
        this._map
          .latLngToContainerPoint(bounds.getSouthEast())
          .multiplyBy(-1)
          .add(this._map.getSize())
      );

      this._viscosity = Math.min(
        1.0,
        Math.max(0.0, this._map.options.maxBoundsViscosity)
      );
    } else {
      this._offsetLimit = null;
    }
    // 触发事件
    map.fire("movestart").fire("dragstart");

    // 初始化惯性数据
    if (map.options.inertia) {
      this._positions = [];
      this._times = [];
    }
  },
  _onDrag: function (e) {
    // 记录位置数据用于惯性计算
    if (this._map.options.inertia) {
      var time = (this._lastTime = +new Date()),
        pos = (this._lastPos =
          this._draggable._absPos || this._draggable._newPos);

      this._positions.push(pos);
      this._times.push(time);

      this._prunePositions(time); // 修剪旧数据
    }

    // 先后触发move和drag类型事件
    this._map.fire("move", e).fire("drag", e);
  },
  _prunePositions: function (time) {
    // 保留最近50ms内的位置数据
    while (this._positions.length > 1 && time - this._times[0] > 50) {
      this._positions.shift();
      this._times.shift();
    }
  },
  _onZoomEnd: function () {
    // 计算惯性速度并启动惯性动画
    var pxCenter = this._map.getSize().divideBy(2),
      pxWorldCenter = this._map.latLngToLayerPoint([0, 0]);

    this._initialWorldOffset = pxWorldCenter.subtract(pxCenter).x;
    this._worldWidth = this._map.getPixelWorldBounds().getSize().x;
  },
  _viscousLimit: function (value, threshold) {
    // 应用粘滞系数调整边界附近的移动
    return value - (value - threshold) * this._viscosity;
  },
  // 边界限制
  _onPreDragLimit: function () {
    if (!this._viscosity || !this._offsetLimit) {
      return;
    }

    var offset = this._draggable._newPos.subtract(this._draggable._startPos);

    var limit = this._offsetLimit;
    // 应用粘滞系数调整偏移量
    if (offset.x < limit.min.x) {
      offset.x = this._viscousLimit(offset.x, limit.min.x);
    }
    if (offset.y < limit.min.y) {
      offset.y = this._viscousLimit(offset.y, limit.min.y);
    }
    if (offset.x > limit.max.x) {
      offset.x = this._viscousLimit(offset.x, limit.max.x);
    }
    if (offset.y > limit.max.y) {
      offset.y = this._viscousLimit(offset.y, limit.max.y);
    }

    // 更新最终位置
    this._draggable._newPos = this._draggable._startPos.add(offset);
  },
  // 世界循环
  _onPreDragWrap: function () {
    var worldWidth = this._worldWidth,
      halfWidth = Math.round(worldWidth / 2),
      dx = this._initialWorldOffset,
      x = this._draggable._newPos.x,
      // 计算水平循环后的新 x 坐标
      newX1 = ((x - halfWidth + dx) % worldWidth) + halfWidth - dx,
      newX2 = ((x + halfWidth + dx) % worldWidth) - halfWidth - dx,
      newX = Math.abs(newX1 + dx) < Math.abs(newX2 + dx) ? newX1 : newX2;

    this._draggable._absPos = this._draggable._newPos.clone();

    // 选择最近的可行位置
    this._draggable._newPos.x = newX;
  },
  _onDragEnd: function () {},
});

Map.addInitHook("addHandler", "drag", Drag);

源码详解

  1. 全局配置

    • inertia:手指松开后地图继续滑动的惯性效果
    • maxBoundsViscosity:当拖拽接近边界时,移动会变得“粘滞”,越接近边界阻力越大
  2. Drag 处理器定义

    • addHooks:启用拖拽功能,绑定事件
    • removeHooks:禁用拖拽功能,解绑事件
    • Draggable:leaflet 内部处理拖拽逻辑的类,负责底层事件处理和坐标计算
    • predrag事件:在拖拽前触发,用于调整位置(如边界限制、世界循环)
  3. 拖拽事件处理

    • 拖拽开始(_onDragStart)
      • _offsetLimit:地理边界对应的容器坐标范围,限制拖拽偏移量
      • _viscosity:边界粘滞系数,值越大拖拽越难超出边界
    • 拖拽中(_onDrag)
      • 惯性数据采集:记录拖拽过程中的位置和时间,用于计算松手后的惯性速度
    • 拖拽结束(_onDragEnd)
      • 惯性计算:根据最后记录的移动速度和方向,触发惯性滑动动画
      • panBy:地图平移方法,结合动画参数实现平滑滑动
  4. 边界限制(_onPreDragLimit)

    • 粘滞效果:当拖拽接近边界时,实际偏移量会逐渐减小,形成难以拖出边界的手感
    • 公式解释: value-(value - threshold) * viscosity使越接近阈值的移动越缓慢
  5. 世界无缝循环(_onPreDragWrap)

    • 实现原理:当地图水平拖拽超过世界宽度时,通过取模运算将坐标“循环”到另一侧,实现无缝滚动
    • 适用场景:地图投影为全球可重复(如墨卡托投影),worldCopyJumptrue时生效

总结

  1. 事件驱动架构 ​​:
  • 通过 Draggable 处理底层指针事件,向上抛出 dragstartdragdragend 事件。
  • predrag 阶段调整位置,实现边界限制和世界循环。
  1. ​​ 惯性滑动 ​​:
  • 记录拖拽末段的速度和方向,松手后触发缓动动画。
  • 公式结合 inertiaDecelerationeaseLinearity 控制动画曲线。​
  1. ​ 边界粘滞 ​​:
  • 通过 maxBoundsViscosity 实现非线性阻力,提升用户体验。​​
  1. 世界循环 ​​:
  • 数学计算确保拖拽到边缘时无缝跳转,支持无限水平滚动。

Drag处理器是 Leaflet 实现流畅拖拽交互的核心,结合数学计算和动画优化,提供了接近原生应用的地图操作体验


网站公告

今日签到

点亮在社区的每一天
去签到