概述
TouchZoom
是 Map
类的一个扩展方法,是 Leaflet 实现触摸设备双指缩放的处理器,用于在移动端通过双指手势控制地图的缩放。
源码分析
源码实现
TouchZoom
的源码实现如下:
Map.mergeOptions({
touchZoom: Browser.touch, // 仅在触摸设备上启用
bounceAtZoomLimits: true, // 缩放超出限制时是否回弹
});
export var TouchZoom = Handler.extend({
addHooks: function () {
// 添加 CSS 类(可能用于样式或标记状态)
DomUtil.addClass(this._map._container, "leaflet-touch-zoom");
// 监听触摸开始事件
DomEvent.on(this._map._container, "touchstart", this._onTouchStart, this);
},
removeHooks: function () {
// 移除 CSS 类和事件监听
DomUtil.removeClass(this._map._container, "leaflet-touch-zoom");
DomEvent.off(this._map._container, "touchstart", this._onTouchStart, this);
},
_onTouchStart: function (e) {
var map = this._map;
// 检查是否双指触摸,且地图未在动画中
if (
!e.touches ||
e.touches.length !== 2 ||
map._animating ||
this._zooming
) {
return;
}
// 计算两指初始位置(转换为地图容器坐标)
var p1 = map.mouseEventToContainerPoint(e.touches[0]),
p2 = map.mouseEventToContainerPoint(e.touches[1]);
// 确定缩放中心点
this._centerPoint = map.getSize()._divideBy(2); // 地图中心
this._startLatLng = map.containerPointToLatLng(this._centerPoint);
// 如果非center模式,则以双指中点为中心
if (map.options.touchZoom !== "center") {
this._pinchStartLatLng = map.containerPointToLatLng(
p1.add(p2)._divideBy(2)
);
}
// 记录初始距离和缩放级别
this._startDist = p1.distanceTo(p2);
this._startZoom = map.getZoom();
this._moved = false;
this._zooming = true;
// 停止地图当前动画
map._stop();
// 绑定文档级触摸事件
DomEvent.on(document, "touchmove", this._onTouchMove, this);
DomEvent.on(document, "touchend touchcancel", this._onTouchEnd, this);
DomEvent.preventDefault(e); // 阻止默认行为
},
_onTouchMove: function (e) {
// 计算当前两指距离,计算缩放比例
if (!e.touches || e.touches.length !== 2 || !this._zooming) {
return;
}
var map = this._map,
p1 = map.mouseEventToContainerPoint(e.touches[0]),
p2 = map.mouseEventToContainerPoint(e.touches[1]),
scale = p1.distanceTo(p2) / this._startDist;
// 根据比例计算目标缩放级别
this._zoom = map.getScaleZoom(scale, this._startZoom);
// 处理缩放限制(若不允许回弹,则强制限制在min/max)
if (
!map.options.bounceAtZoomLimits &&
((this._zoom < map.getMinZoom() && scale < 1) ||
(this._zoom > map.getMaxZoom() && scale > 1))
) {
this._zoom = map._limitZoom(this._zoom);
}
// 根据模式计算新的中心点
if (map.options.touchZoom === "center") {
this._center = this._startLatLng;
if (scale === 1) {
return;
}
} else {
// 计算双指中点偏移量,重新投影到目标缩放级别下的坐标
var delta = p1._add(p2)._divideBy(2)._subtract(this._centerPoint);
if (scale === 1 && delta.x === 0 && delta.y === 0) {
return;
}
this._center = map.unproject(
map.project(this._pinchStartLatLng, this._zoom).subtract(delta),
this._zoom
);
}
// 触发地图移动(若未移动过,先触发`_moveStart`方法)
if (!this._moved) {
map._moveStart(true, false);
this._moved = true;
}
// 使用动画帧优化性能,平滑更新地图视图
Util.cancelAnimFrame(this._animRequest);
var moveFn = Util.bind(
map._move,
map,
this._center,
this._zoom,
{ pinch: true, round: false },
undefined
);
this._animRequest = Util.requestAnimFrame(moveFn, this, true);
DomEvent.preventDefault(e);
},
_onTouchEnd: function () {
// 若未移动或未在缩放状态,则直接返回
if (!this._moved || !this._zooming) {
this._zooming = false;
return;
}
// 清理状态和事件监听
this._zooming = false;
Util.cancelAnimFrame(this._animRequest);
DomEvent.off(document, "touchmove", this._onTouchMove, this);
DomEvent.off(document, "touchend touchcancel", this._onTouchEnd, this);
// 根据配置执行最终缩放动画或直接重置视图
if (this._map.options.zoomAnimation) {
this._map._animateZoom(
this._center,
this._map._limitZoom(this._zoom),
true,
this._map.options.zoomSnap
);
} else {
this._map._resetView(this._center, this._map._limitZoom(this._zoom));
}
},
});
Map.addInitHook("addHandler", "touchZoom", TouchZoom);
源码详解
全局配置
touchZoom
:根据浏览器是否支持触摸事件启用功能bounceAtZoomLimits
:当缩放到最小/最大级别时是否允许短暂回弹(类似惯性效果)
处理器定义(
TouchZoom
)addHooks
: 绑定touchstart
事件到_onTouchStart
。removeHooks
: 清理操作,确保处理器禁用时解除绑定。
触摸开始(
_onTouchStart
)- 仅处理双指触摸
- 根据配置(
touchZoom
模式)计算缩放中心点center
模式:始终以地图中心缩放- 非
center
模式:以双指中点为中心
- 记录初始距离和缩放级别,为后续计算缩放比例做准备
- 绑定文档级事件,确保触摸点在移动时仍能触发
触摸移动(
_onTouchMove
)- 计算缩放比例:通过两指距离变化(
scale
)计算新的缩放级别 - 处理缩放限制:若
bounceAtZoomLimits
为false
,则强制限制在min/max
级别 - 计算中心点:
center
模式:保持地图中心不变- 非
center
模式:根据双指中点偏移量(delta
)重新投影到目标缩放级别下的坐标
- 优化渲染:使用
requestAnimFrame
避免频繁更新导致的卡顿
- 计算缩放比例:通过两指距离变化(
触摸结束(
_onTouchEnd
)- 状态清理:取消动画帧,解除事件监听
- 应用最终视图:
- 如果启用缩放动画,平滑过渡到目标缩放级别
- 否则直接重置视图
注册处理器
通过Map.addInitHook
方法将TouchZoom
处理器添加到地图的初始化流程中,当在触摸设备上时,会实例化TouchZoom
类,使其生效。
总结
双指中心点计算 :
- 支持两种模式:固定地图中心或动态跟随双指中点。
- 动态模式下,通过
project
和unproject
方法处理不同缩放级别的坐标转换。
性能优化 :
- 使用
requestAnimFrame
避免过度渲染。 - 仅在必要时触发
_moveStart
和_move
,减少计算量。
- 使用
边界处理 :
- 根据
bounceAtZoomLimits
决定是否允许短暂超出缩放限制。 - 最终缩放级别会通过
_limitZoom
确保在合法范围内。
- 根据
事件协作 :
与 Leaflet 内部方法(如_stop
,_animateZoom
)协作,确保与其他操作(如拖拽、其他缩放控件)互不冲突。
这段代码实现了流畅的触摸缩放交互,是 Leaflet 在移动端的重要功能之一。