概述
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);
源码详解
全局配置
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 实现流畅拖拽交互的核心,结合数学计算和动画优化,提供了接近原生应用的地图操作体验