Jinuss's blog Jinuss's blog
首页
  • 源码合集

    • Leaflet源码分析
    • Openlayers源码合集
    • vue3源码
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • 学习
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

东流

Web、WebGIS技术博客
首页
  • 源码合集

    • Leaflet源码分析
    • Openlayers源码合集
    • vue3源码
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • 学习
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 框架

  • core模块

  • dom模块

  • control

  • geometry

  • geo

  • layer

  • Map

    • Map类
    • Map类扩展方法之BoxZoom
    • Map类扩展方法之DoubleClickZoom
    • Map类扩展方法之Drag
      • 概述
      • 源码分析
        • 源码实现
        • 源码详解
      • 总结
    • Map类扩展方法之Keyboard
    • Map类扩展方法之ScrollWheelZoom
    • Map类扩展方法之TapHold
    • Map类扩展方法之TouchZoom
  • 《Leaflet源码》笔记
  • Map
东流
2025-04-07
目录

Map类扩展方法之Drag

# 概述

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169

# 源码详解

  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)

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

# 总结

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

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

编辑 (opens new window)
上次更新: 2025/04/17, 02:36:24
Map类扩展方法之DoubleClickZoom
Map类扩展方法之Keyboard

← Map类扩展方法之DoubleClickZoom Map类扩展方法之Keyboard→

最近更新
01
GeoJSON
05-08
02
Circle
04-15
03
CircleMarker
04-15
更多文章>
Theme by Vdoing | Copyright © 2024-2025 东流 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式