源码分析之Leaflet中Map类扩展方法之TouchZoom

发布于:2025-04-18 ⋅ 阅读:(72) ⋅ 点赞:(0)

概述

TouchZoomMap 类的一个扩展方法,是 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. 全局配置

    • touchZoom:根据浏览器是否支持触摸事件启用功能
    • bounceAtZoomLimits:当缩放到最小/最大级别时是否允许短暂回弹(类似惯性效果)
  2. 处理器定义(TouchZoom)

    • addHooks​​: 绑定 touchstart 事件到 _onTouchStart
    • ​​removeHooks​​: 清理操作,确保处理器禁用时解除绑定。
  3. 触摸开始(_onTouchStart)

    • 仅处理双指触摸
    • 根据配置(touchZoom模式)计算缩放中心点
      • center模式:始终以地图中心缩放
      • center模式:以双指中点为中心
    • 记录初始距离和缩放级别,为后续计算缩放比例做准备
    • 绑定文档级事件,确保触摸点在移动时仍能触发
  4. 触摸移动(_onTouchMove)

    • 计算缩放比例:通过两指距离变化(scale)计算新的缩放级别
    • 处理缩放限制:若bounceAtZoomLimitsfalse,则强制限制在min/max级别
    • 计算中心点:
      • center模式:保持地图中心不变
      • center模式:根据双指中点偏移量(delta)重新投影到目标缩放级别下的坐标
    • 优化渲染:使用requestAnimFrame避免频繁更新导致的卡顿
  5. 触摸结束(_onTouchEnd)

    • 状态清理:取消动画帧,解除事件监听
    • 应用最终视图:
      • 如果启用缩放动画,平滑过渡到目标缩放级别
      • 否则直接重置视图
  6. 注册处理器
    通过Map.addInitHook方法将TouchZoom处理器添加到地图的初始化流程中,当在触摸设备上时,会实例化TouchZoom类,使其生效。

总结

  1. ​ 双指中心点计算 ​​:

    • 支持两种模式:固定地图中心或动态跟随双指中点。
    • 动态模式下,通过 projectunproject 方法处理不同缩放级别的坐标转换。
  2. 性能优化 ​​:

    • 使用 requestAnimFrame 避免过度渲染。
    • 仅在必要时触发 _moveStart_move,减少计算量。
  3. ​ 边界处理 ​​:

    • 根据 bounceAtZoomLimits 决定是否允许短暂超出缩放限制。
    • 最终缩放级别会通过 _limitZoom 确保在合法范围内。
      ​​
  4. 事件协作 ​​:
    与 Leaflet 内部方法(如_stop, _animateZoom)协作,确保与其他操作(如拖拽、其他缩放控件)互不冲突。

这段代码实现了流畅的触摸缩放交互,是 Leaflet 在移动端的重要功能之一。


网站公告

今日签到

点亮在社区的每一天
去签到