检测捏的最简单方法

104

这是一个WEB APP而不是本地应用程序。请勿使用Objective-C NS命令。

因此,我需要在iOS上检测“pinch”手势事件。问题是,我看到的所有插件或方法都是(通常)使用jQuery进行手势或多点触摸事件处理,并且对于每个手势类型都需要一个额外的插件。我的应用程序非常庞大,我非常注重代码的简洁性。我只需要检测一下缩放手势,使用类似jGesture这样的方式对我的简单需求来说过于臃肿。

此外,我对如何手动检测pinch手势的方法了解有限。我可以获取两个手指的位置,但无法正确地检测它。是否有人有一个简单的片段只检测pinch手势?


我找到的最佳解决方案在Mozilla的文档中。Pinch Zoom Gesture文章非常好地描述和解决了这个问题。 - user11484628
9个回答

175

想一想什么是“pinch”事件:两个手指在一个元素上,朝着或远离彼此移动。手势事件据我所知是相当新的标准,因此最安全的方法可能是使用如下的触摸事件:

ontouchstart事件)

if (e.touches.length === 2) {
    scaling = true;
    pinchStart(e);
}

(ontouchmove 事件)

if (scaling) {
    pinchMove(e);
}

(ontouchend事件)

if (scaling) {
    pinchEnd(e);
    scaling = false;
}

使用hypot函数可以获得两个手指之间的距离:

var dist = Math.hypot(
    e.touches[0].pageX - e.touches[1].pageX,
    e.touches[0].pageY - e.touches[1].pageY);

3
为什么要自己编写捏合手势检测的代码?iOS的Webkit中已经内置了这个功能。自己编写的实现不好,因为无法区分双指滑动和捏合手势。这不是一个好的建议。 - mmaclaurin
44
因为 Webkit 并非一直都有捏合检测功能(如果我说错了,请纠正我),而且并非所有触摸屏都使用 Webkit,有时候也不需要检测滑动事件。原帖作者想要一个简单的解决方案,而不需要死板的库函数。 - Jeffrey Sweeney
6
在考虑其他平台时,OP提到了iOS,但这是最佳答案。除非你在距离计算中省略了平方根部分,我加上了它。 - undefined
3
这是有意为之的;sqrt可能会很耗费时间,通常你只需要知道你的手指移动了多少。但是...我说了勾股定理,而我并没有严格使用它;) - Jeffrey Sweeney
3
@mmaclaurin 只需检查(deltaX * deltaY <= 0),这样您就可以检测到所有的捏合手势,而不是双指滑动。 - Dolma
显示剩余7条评论

76

您想使用 gesturestartgesturechangegestureend 事件。这些事件会在屏幕上有2个或更多个手指触摸时触发。

根据您需要使用捏合手势的情况,您需要调整相应的方法。可以检查 scale 倍增器以确定用户的捏合手势有多明显。有关 scale 属性的详细信息,请参见苹果公司的 TouchEvent 文档

node.addEventListener('gestureend', function(e) {
    if (e.scale < 1.0) {
        // User moved fingers closer together
    } else if (e.scale > 1.0) {
        // User moved fingers further apart
    }
}, false);

如果你需要让你的应用程序更具响应性,还可以拦截 gesturechange 事件来检测捏合手势。


77
我知道这个问题特别涉及到iOS,但问题的标题是通用的“最简单的检测缩放手势的方法”。gesturestart、gesturechange和gestureend事件只适用于iOS,无法跨平台使用。它们在Android或其他触摸浏览器上不会触发。要实现跨平台,可以使用touchstart、touchmove和touchend事件,就像这个答案中所示:https://dev59.com/a2gu5IYBdhLWcg3wpYcj#11183333。 - Phil McCullick
7
如果您想要支持所有移动浏览器的最简单方法,最好使用hammer.js。 - Dan Herbert
4
我使用了 jQuery 的 $(selector).on('gestureend',...),并且不得不使用 e.originalEvent.scale 替代 e.scale - Chad von Nau
3
这是因为jQuery的事件对象是一个“规范化的W3C事件对象”。W3C事件对象不包括scale属性,这是一种特定于供应商的属性。虽然我的答案包括使用纯JS完成任务的最简单方法,但如果你已经在使用JS框架,最好使用hammer.js,因为它将为你提供更好的API。 - Dan Herbert
1
@superuberduper IE8/9 没有办法检测到缩放手势。直到 IE10 才添加了触摸 API。虽然原问题明确提到了 iOS,但为了跨浏览器处理此问题,您应该使用 hammer.js 框架,它可以抽象出跨浏览器的差异。 - Dan Herbert
显示剩余2条评论

31

我在看到Dan的回答之前,实际上已经找到并实现了hammer.js。 Hammer非常酷。 - Fresheyeball
它看起来很酷,但演示并不太流畅。缩放后尝试平移感觉非常卡顿。 - Alex K
3
值得注意的是,截至撰写本文时(尤其是 Android 系统),Hammer 存在大量严重的未解决 bug。这是值得考虑的事情。 - Single Entity
3
我也一样有问题。尝试了Hammer,最终使用了Jeffrey的解决方案。 - Paul
链接看起来也挂了。 - tnrich

5

不幸的是,跨浏览器检测捏合手势并不像人们所希望的那样简单,但HammerJS使其变得更加容易!

请查看使用HammerJS进行捏合缩放和平移的演示。这个例子已经在Android,iOS和Windows Phone上进行了测试。

你可以在Pinch Zoom and Pan with HammerJS找到源代码。

为了您的方便,这里是源代码:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport"
        content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
  <title>Pinch Zoom</title>
</head>

<body>

  <div>

    <div style="height:150px;background-color:#eeeeee">
      Ignore this area. Space is needed to test on the iPhone simulator as pinch simulation on the
      iPhone simulator requires the target to be near the middle of the screen and we only respect
      touch events in the image area. This space is not needed in production.
    </div>

    <style>

      .pinch-zoom-container {
        overflow: hidden;
        height: 300px;
      }

      .pinch-zoom-image {
        width: 100%;
      }

    </style>

    <script src="https://hammerjs.github.io/dist/hammer.js"></script>

    <script>

      var MIN_SCALE = 1; // 1=scaling when first loaded
      var MAX_SCALE = 64;

      // HammerJS fires "pinch" and "pan" events that are cumulative in nature and not
      // deltas. Therefore, we need to store the "last" values of scale, x and y so that we can
      // adjust the UI accordingly. It isn't until the "pinchend" and "panend" events are received
      // that we can set the "last" values.

      // Our "raw" coordinates are not scaled. This allows us to only have to modify our stored
      // coordinates when the UI is updated. It also simplifies our calculations as these
      // coordinates are without respect to the current scale.

      var imgWidth = null;
      var imgHeight = null;
      var viewportWidth = null;
      var viewportHeight = null;
      var scale = null;
      var lastScale = null;
      var container = null;
      var img = null;
      var x = 0;
      var lastX = 0;
      var y = 0;
      var lastY = 0;
      var pinchCenter = null;

      // We need to disable the following event handlers so that the browser doesn't try to
      // automatically handle our image drag gestures.
      var disableImgEventHandlers = function () {
        var events = ['onclick', 'onmousedown', 'onmousemove', 'onmouseout', 'onmouseover',
                      'onmouseup', 'ondblclick', 'onfocus', 'onblur'];

        events.forEach(function (event) {
          img[event] = function () {
            return false;
          };
        });
      };

      // Traverse the DOM to calculate the absolute position of an element
      var absolutePosition = function (el) {
        var x = 0,
          y = 0;

        while (el !== null) {
          x += el.offsetLeft;
          y += el.offsetTop;
          el = el.offsetParent;
        }

        return { x: x, y: y };
      };

      var restrictScale = function (scale) {
        if (scale < MIN_SCALE) {
          scale = MIN_SCALE;
        } else if (scale > MAX_SCALE) {
          scale = MAX_SCALE;
        }
        return scale;
      };

      var restrictRawPos = function (pos, viewportDim, imgDim) {
        if (pos < viewportDim/scale - imgDim) { // too far left/up?
          pos = viewportDim/scale - imgDim;
        } else if (pos > 0) { // too far right/down?
          pos = 0;
        }
        return pos;
      };

      var updateLastPos = function (deltaX, deltaY) {
        lastX = x;
        lastY = y;
      };

      var translate = function (deltaX, deltaY) {
        // We restrict to the min of the viewport width/height or current width/height as the
        // current width/height may be smaller than the viewport width/height

        var newX = restrictRawPos(lastX + deltaX/scale,
                                  Math.min(viewportWidth, curWidth), imgWidth);
        x = newX;
        img.style.marginLeft = Math.ceil(newX*scale) + 'px';

        var newY = restrictRawPos(lastY + deltaY/scale,
                                  Math.min(viewportHeight, curHeight), imgHeight);
        y = newY;
        img.style.marginTop = Math.ceil(newY*scale) + 'px';
      };

      var zoom = function (scaleBy) {
        scale = restrictScale(lastScale*scaleBy);

        curWidth = imgWidth*scale;
        curHeight = imgHeight*scale;

        img.style.width = Math.ceil(curWidth) + 'px';
        img.style.height = Math.ceil(curHeight) + 'px';

        // Adjust margins to make sure that we aren't out of bounds
        translate(0, 0);
      };

      var rawCenter = function (e) {
        var pos = absolutePosition(container);

        // We need to account for the scroll position
        var scrollLeft = window.pageXOffset ? window.pageXOffset : document.body.scrollLeft;
        var scrollTop = window.pageYOffset ? window.pageYOffset : document.body.scrollTop;

        var zoomX = -x + (e.center.x - pos.x + scrollLeft)/scale;
        var zoomY = -y + (e.center.y - pos.y + scrollTop)/scale;

        return { x: zoomX, y: zoomY };
      };

      var updateLastScale = function () {
        lastScale = scale;
      };

      var zoomAround = function (scaleBy, rawZoomX, rawZoomY, doNotUpdateLast) {
        // Zoom
        zoom(scaleBy);

        // New raw center of viewport
        var rawCenterX = -x + Math.min(viewportWidth, curWidth)/2/scale;
        var rawCenterY = -y + Math.min(viewportHeight, curHeight)/2/scale;

        // Delta
        var deltaX = (rawCenterX - rawZoomX)*scale;
        var deltaY = (rawCenterY - rawZoomY)*scale;

        // Translate back to zoom center
        translate(deltaX, deltaY);

        if (!doNotUpdateLast) {
          updateLastScale();
          updateLastPos();
        }
      };

      var zoomCenter = function (scaleBy) {
        // Center of viewport
        var zoomX = -x + Math.min(viewportWidth, curWidth)/2/scale;
        var zoomY = -y + Math.min(viewportHeight, curHeight)/2/scale;

        zoomAround(scaleBy, zoomX, zoomY);
      };

      var zoomIn = function () {
        zoomCenter(2);
      };

      var zoomOut = function () {
        zoomCenter(1/2);
      };

      var onLoad = function () {

        img = document.getElementById('pinch-zoom-image-id');
        container = img.parentElement;

        disableImgEventHandlers();

        imgWidth = img.width;
        imgHeight = img.height;
        viewportWidth = img.offsetWidth;
        scale = viewportWidth/imgWidth;
        lastScale = scale;
        viewportHeight = img.parentElement.offsetHeight;
        curWidth = imgWidth*scale;
        curHeight = imgHeight*scale;

        var hammer = new Hammer(container, {
          domEvents: true
        });

        hammer.get('pinch').set({
          enable: true
        });

        hammer.on('pan', function (e) {
          translate(e.deltaX, e.deltaY);
        });

        hammer.on('panend', function (e) {
          updateLastPos();
        });

        hammer.on('pinch', function (e) {

          // We only calculate the pinch center on the first pinch event as we want the center to
          // stay consistent during the entire pinch
          if (pinchCenter === null) {
            pinchCenter = rawCenter(e);
            var offsetX = pinchCenter.x*scale - (-x*scale + Math.min(viewportWidth, curWidth)/2);
            var offsetY = pinchCenter.y*scale - (-y*scale + Math.min(viewportHeight, curHeight)/2);
            pinchCenterOffset = { x: offsetX, y: offsetY };
          }

          // When the user pinch zooms, she/he expects the pinch center to remain in the same
          // relative location of the screen. To achieve this, the raw zoom center is calculated by
          // first storing the pinch center and the scaled offset to the current center of the
          // image. The new scale is then used to calculate the zoom center. This has the effect of
          // actually translating the zoom center on each pinch zoom event.
          var newScale = restrictScale(scale*e.scale);
          var zoomX = pinchCenter.x*newScale - pinchCenterOffset.x;
          var zoomY = pinchCenter.y*newScale - pinchCenterOffset.y;
          var zoomCenter = { x: zoomX/newScale, y: zoomY/newScale };

          zoomAround(e.scale, zoomCenter.x, zoomCenter.y, true);
        });

        hammer.on('pinchend', function (e) {
          updateLastScale();
          updateLastPos();
          pinchCenter = null;
        });

        hammer.on('doubletap', function (e) {
          var c = rawCenter(e);
          zoomAround(2, c.x, c.y);
        });

      };

    </script>

    <button onclick="zoomIn()">Zoom In</button>
    <button onclick="zoomOut()">Zoom Out</button>

    <div class="pinch-zoom-container">
      <img id="pinch-zoom-image-id" class="pinch-zoom-image" onload="onLoad()"
           src="https://hammerjs.github.io/assets/img/pano-1.jpg">
    </div>


  </div>

</body>
</html>


5
检测任何元素上的双指捏合缩放,无需使用第三方库(例如Hammer.js,注意,Hammer在滚动方面存在问题!),简单且不麻烦。
function onScale(el, callback) {
    let hypo = undefined;

    el.addEventListener('touchmove', function(event) {
        if (event.targetTouches.length === 2) {
            let hypo1 = Math.hypot((event.targetTouches[0].pageX - event.targetTouches[1].pageX),
                (event.targetTouches[0].pageY - event.targetTouches[1].pageY));
            if (hypo === undefined) {
                hypo = hypo1;
            }
            callback(hypo1/hypo);
        }
    }, false);


    el.addEventListener('touchend', function(event) {
        hypo = undefined;
    }, false);
}

2
似乎最好使用event.touches而不是event.targetTouches - TheStoryCoder

4

最简单的方法是响应“wheel”事件。

您需要调用ev.preventDefault()来防止浏览器进行全屏缩放。

浏览器会合成轨迹板上捏的“wheel”事件,作为奖励,您还可以处理鼠标滚轮事件。这是映射应用程序处理它的方式。

更多细节请参见我的示例:

let element = document.getElementById('el');
let scale = 1.0;
element.addEventListener('wheel', (ev) => {
  // This is crucial. Without it, the browser will do a full page zoom
  ev.preventDefault();

  // This is an empirically determined heuristic.
  // Unfortunately I don't know of any way to do this better.
  // Typical deltaY values from a trackpad pinch are under 1.0
  // Typical deltaY values from a mouse wheel are more than 100.
  let isPinch = Math.abs(ev.deltaY) < 50;

  if (isPinch) {
    // This is a pinch on a trackpad
    let factor = 1 - 0.01 * ev.deltaY;
    scale *= factor;
    element.innerText = `Pinch: scale is ${scale}`;
  } else {
    // This is a mouse wheel
    let strength = 1.4;
    let factor = ev.deltaY < 0 ? strength : 1.0 / strength;
    scale *= factor;
    element.innerText = `Mouse: scale is ${scale}`;
  }
});
<div id='el' style='width:400px; height:300px; background:#ffa'>
  Scale: 1.0
</div>


感谢 Ed_ 提供 abs() 函数。 - Ben Harper
绝对是最好的答案。谢谢! - Tomáš Wróbel
4
轮子事件不会触发移动设备的捏合缩放(我刚试过)。但是对于触控板缩放,它确实会触发。 - Kenji Miwa

1

这些答案都没有达到我想要的效果,所以最终我自己编写了一些代码。我希望在我的网站上使用 MacBookPro 触摸板对图像进行捏合缩放。下面的代码(需要使用 jQuery)似乎在 Chrome 和 Edge 中可以正常工作。也许对其他人有用。

function setupImageEnlargement(el)
{
    // "el" represents the image element, such as the results of document.getElementByd('image-id')
    var img = $(el);
    $(window, 'html', 'body').bind('scroll touchmove mousewheel', function(e)
    {
        //TODO: need to limit this to when the mouse is over the image in question

        //TODO: behavior not the same in Safari and FF, but seems to work in Edge and Chrome

        if (typeof e.originalEvent != 'undefined' && e.originalEvent != null
            && e.originalEvent.wheelDelta != 'undefined' && e.originalEvent.wheelDelta != null)
        {
            e.preventDefault();
            e.stopPropagation();
            console.log(e);
            if (e.originalEvent.wheelDelta > 0)
            {
                // zooming
                var newW = 1.1 * parseFloat(img.width());
                var newH = 1.1 * parseFloat(img.height());
                if (newW < el.naturalWidth && newH < el.naturalHeight)
                {
                    // Go ahead and zoom the image
                    //console.log('zooming the image');
                    img.css(
                    {
                        "width": newW + 'px',
                        "height": newH + 'px',
                        "max-width": newW + 'px',
                        "max-height": newH + 'px'
                    });
                }
                else
                {
                    // Make image as big as it gets
                    //console.log('making it as big as it gets');
                    img.css(
                    {
                        "width": el.naturalWidth + 'px',
                        "height": el.naturalHeight + 'px',
                        "max-width": el.naturalWidth + 'px',
                        "max-height": el.naturalHeight + 'px'
                    });
                }
            }
            else if (e.originalEvent.wheelDelta < 0)
            {
                // shrinking
                var newW = 0.9 * parseFloat(img.width());
                var newH = 0.9 * parseFloat(img.height());

                //TODO: I had added these data-attributes to the image onload.
                // They represent the original width and height of the image on the screen.
                // If your image is normally 100% width, you may need to change these values on resize.
                var origW = parseFloat(img.attr('data-startwidth'));
                var origH = parseFloat(img.attr('data-startheight'));

                if (newW > origW && newH > origH)
                {
                    // Go ahead and shrink the image
                    //console.log('shrinking the image');
                    img.css(
                    {
                        "width": newW + 'px',
                        "height": newH + 'px',
                        "max-width": newW + 'px',
                        "max-height": newH + 'px'
                    });
                }
                else
                {
                    // Make image as small as it gets
                    //console.log('making it as small as it gets');
                    // This restores the image to its original size. You may want
                    //to do this differently, like by removing the css instead of defining it.
                    img.css(
                    {
                        "width": origW + 'px',
                        "height": origH + 'px',
                        "max-width": origW + 'px',
                        "max-height": origH + 'px'
                    });
                }
            }
        }
    });
}

1

我的答案受到Jeffrey的答案启发。虽然那个答案提供了一个更抽象的解决方案,但我试图提供更具体的步骤,以便潜在地实现它。这只是一个指南,可以更优雅地实现。有关更详细的示例,请查看MDN Web Docs的tutorial

HTML:

<div id="zoom_here">....</div>

JS

<script>
var dist1=0;
function start(ev) {
           if (ev.targetTouches.length == 2) {//check if two fingers touched screen
               dist1 = Math.hypot( //get rough estimate of distance between two fingers
                ev.touches[0].pageX - ev.touches[1].pageX,
                ev.touches[0].pageY - ev.touches[1].pageY);                  
           }
    
    }
    function move(ev) {
           if (ev.targetTouches.length == 2 && ev.changedTouches.length == 2) {
                 // Check if the two target touches are the same ones that started
               var dist2 = Math.hypot(//get rough estimate of new distance between fingers
                ev.touches[0].pageX - ev.touches[1].pageX,
                ev.touches[0].pageY - ev.touches[1].pageY);
                //alert(dist);
                if(dist1>dist2) {//if fingers are closer now than when they first touched screen, they are pinching
                  alert('zoom out');
                }
                if(dist1<dist2) {//if fingers are further apart than when they first touched the screen, they are making the zoomin gesture
                   alert('zoom in');
                }
           }
           
    }
        document.getElementById ('zoom_here').addEventListener ('touchstart', start, false);
        document.getElementById('zoom_here').addEventListener('touchmove', move, false);
</script>

1
添加放大缩小逻辑也会非常有帮助。 - user11484628

0

正如Jeffrey Sweeney所评论的那样,这是一个完整的示例,展示了如何在您的类中实现。

this.touch.isPinch = false;
this.touc.pinchStart = 0;

this.touch.onTouchStart = (e) => {
   if (e.touches.length === 2) {
    this.touch.pinchStart = Math.hypot(e.touches[0].pageX - e.touches[1].pageX, e.touches[0].pageY - e.touches[1].pageY);
    this.touch.isScaling = true;
  }
}

this.touch.onTouchMove = (e) => {
    if (this.touch.isScaling) {
      const distance = Math.hypot(e.touches[0].pageX - e.touches[1].pageX, e.touches[0].pageY - e.touches[1].pageY);

      if (this.touch.pinchStart >= 200 && distance <= 90) this.touchPichOut(); //call function for pinchOut
      if (this.touch.pinchStart <= 100 && distance >= 280) this.touchPichIn(); //call function for pinchIn
    }
}

this.touch.onTouchCancel = (e) => {
   this.touch.isScaling = false;
}

this.touch.onTouchEnd = (e) => {
  if (this.touch.isScaling) this.touch.isScaling = false;
}

敬礼


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接