Map类扩展方法之TouchZoom
# 概述
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);
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
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
# 源码详解
全局配置
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 在移动端的重要功能之一。
编辑 (opens new window)
上次更新: 2025/04/17, 02:36:24