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
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
# 源码详解
全局配置
inertia
:手指松开后地图继续滑动的惯性效果maxBoundsViscosity
:当拖拽接近边界时,移动会变得“粘滞”,越接近边界阻力越大
Drag 处理器定义
addHooks
:启用拖拽功能,绑定事件removeHooks
:禁用拖拽功能,解绑事件Draggable
:leaflet 内部处理拖拽逻辑的类,负责底层事件处理和坐标计算predrag
事件:在拖拽前触发,用于调整位置(如边界限制、世界循环)
拖拽事件处理
- 拖拽开始(
_onDragStart
)_offsetLimit
:地理边界对应的容器坐标范围,限制拖拽偏移量_viscosity
:边界粘滞系数,值越大拖拽越难超出边界
- 拖拽中(
_onDrag
)- 惯性数据采集:记录拖拽过程中的位置和时间,用于计算松手后的惯性速度
- 拖拽结束(
_onDragEnd
)- 惯性计算:根据最后记录的移动速度和方向,触发惯性滑动动画
panBy
:地图平移方法,结合动画参数实现平滑滑动
- 拖拽开始(
边界限制(
_onPreDragLimit
)- 粘滞效果:当拖拽接近边界时,实际偏移量会逐渐减小,形成难以拖出边界的手感
- 公式解释:
value-(value - threshold) * viscosity
使越接近阈值的移动越缓慢
世界无缝循环(
_onPreDragWrap
)- 实现原理:当地图水平拖拽超过世界宽度时,通过取模运算将坐标“循环”到另一侧,实现无缝滚动
- 适用场景:地图投影为全球可重复(如墨卡托投影),
worldCopyJump
为true
时生效
# 总结
- 事件驱动架构 :
- 通过
Draggable
处理底层指针事件,向上抛出dragstart
、drag
、dragend
事件。 - 在
predrag
阶段调整位置,实现边界限制和世界循环。
- 惯性滑动 :
- 记录拖拽末段的速度和方向,松手后触发缓动动画。
- 公式结合
inertiaDeceleration
和easeLinearity
控制动画曲线。
- 边界粘滞 :
- 通过
maxBoundsViscosity
实现非线性阻力,提升用户体验。
- 世界循环 :
- 数学计算确保拖拽到边缘时无缝跳转,支持无限水平滚动。
Drag
处理器是 Leaflet 实现流畅拖拽交互的核心,结合数学计算和动画优化,提供了接近原生应用的地图操作体验
编辑 (opens new window)
上次更新: 2025/04/17, 02:36:24