ピンチを検出する最も簡単な方法


85

これは、ネイティブアプリではなくWEBアプリです。Objective-CNSコマンドは使用しないでください。

そのため、iOSで「ピンチ」イベントを検出する必要があります。問題は、ジェスチャまたはマルチタッチイベントを実行するために私が目にするすべてのプラグインまたはメソッドであり、(通常は)jQueryを使用しており、太陽の下でのすべてのジェスチャに対する完全な追加プラグインです。私のアプリケーションは巨大で、コードのデッドウッドに非常に敏感です。必要なのはピンチを検出することだけです。jGestureのようなものを使用することは、私の単純なニーズを肥大化させる方法です。

さらに、ピンチを手動で検出する方法についての理解は限られています。両方の指の位置を取得できますが、これを検出するためのミックスを正しく取得できないようです。誰かがピンチを検出するだけの簡単なスニペットを持っていますか?

回答:


71

あなたは利用したいgesturestartgesturechangegestureendのイベントを。これらは、2本以上の指が画面に触れるたびにトリガーされます。

ピンチジェスチャで何をする必要があるかに応じて、アプローチを調整する必要があります。scale乗算器は、ユーザのピンチジェスチャーがいかに劇的に決定するために検査することができます。参照してくださいAppleのたTouchEventドキュメント方法についての詳細は、scaleプロパティが動作しますが。

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、アプリの応答性を高めるために必要な場合は、イベントをインターセプトしてピンチが発生したときにそれを検出することもできます。


58
この質問は特にiOSに関するものでしたが、質問のタイトルは一般的な「ピンチを検出する最も簡単な方法」です。ジェスチャスタート、ジェスチャチェンジ、ジェスチャエンドイベントはiOS固有であり、クロスプラットフォームでは機能しません。Androidやその他のタッチブラウザでは起動しません。このクロスプラットフォームを実行するには、この回答stackoverflow.com/a/11183333/375690のように、touchstart、touchmove、およびtouchendイベントを使用します。
Phil McCullick 2014

6
@philすべてのモバイルブラウザをサポートする最も簡単な方法を探している場合は、hammer.jsを使用することをお勧めします
Dan Herbert

4
私はjQuery$(selector).on('gestureend',...)を使用し、のe.originalEvent.scale代わりに使用する必要がありましたe.scale
チャドフォンナウ

3
@ChadvonNauこれは、jQueryのイベントオブジェクトが「正規化されたW3Cイベントオブジェクト」であるためです。W3Cイベントオブジェクトが含まれていませんscaleプロパティを。これはベンダー固有のプロパティです。私の答えには、バニラJSでタスクを実行する最も簡単な方法が含まれていますが、すでにJSフレームワークを使用している場合は、はるかに優れたAPIを提供するhammer.jsを使用することをお勧めします。
ダンハーバート

1
@superuberduper IE8 / 9には、ピンチを検出する方法がまったくありません。タッチAPIは、IE10までIEに追加されませんでした。元の質問は特にiOSについて尋ねましたが、ブラウザー間でこれを処理するには、ブラウザー間の違いを抽象化するhammer.jsフレームワークを使用する必要があります。
ダンハーバート

134

pinchイベントとは何かを考えてみてください。要素に2本の指があり、互いに近づいたり遠ざかったりします。私の知る限り、ジェスチャイベントはかなり新しい標準であるため、これを実行する最も安全な方法は、次のようなタッチイベントを使用することです。

ontouchstartイベント)

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

ontouchmoveイベント)

if (scaling) {
    pinchMove(e);
}

ontouchendイベント)

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

2本の指の間の距離を取得するには、次のhypot関数を使用します。

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

1
なぜあなたはあなた自身のピンチ検出を書くのですか?これはiOSWebkitのネイティブ機能です。これは、2本指のスワイプとピンチの違いがわからないため、適切な実装でもありません。良いアドバイスではありません。
mmaclaurin 2012年

34
@mmaclaurinは、webkitが常にピンチ検出を備えているとは限らず(間違っている場合は修正してください)、すべてのタッチスクリーンがwebkitを使用しているわけではなく、スワイプイベントを検出する必要がない場合もあります。OPは、デッドウッドライブラリ関数のないシンプルなソリューションを求めていました。
ジェフリースウィーニー

6
OPはiOSについて言及しましたが、他のプラットフォームを検討する場合、これが最良の答えです。平方根部分を距離計算から除外した場合を除きます。私はそれを置く。
未定義

3
@BrianMortensonそれは意図的なものでした。sqrt費用がかかる可能性があり、通常、指がある程度の大きさで出入りすることを知っておく必要があります。しかし..私はピタゴラスの定理を言いました、そして私はそれを技術的に使用していませんでした;)
ジェフリースウィーニー

2
@mmaclaurin(deltaX * deltaY <= 0)をチェックするだけで、2本の指でスワイプするのではなく、すべてのピンチケースを検出できます。
ドルマ2015年

29

Hammer.jsはずっと!「変換」(ピンチ)を処理します。 http://eightmedia.github.com/hammer.js/

しかし、自分で実装したいのであれば、ジェフリーの答えはかなりしっかりしていると思います。


ダンの答えを見る前に、私は実際にhammer.jsを見つけて実装しました。ハンマーはかなりクールです。
フレッシュアイボール2012年

見た目はかっこいいですが、デモはそれほどスムーズではありませんでした。ズームインしてからパンしようとすると、本当にぎくしゃくした感じがしました。
Alex K

3
ハンマーには未解決のバグがたくさんあり、そのいくつかはこれを書いている時点では非常に深刻です(特にAndroid)。考える価値があります。
単一エンティティ

3
ここでも同じ、バギー。ハンマーを試し、ジェフリーのソリューションを使用することになりました。
ポール

4

残念ながら、ブラウザ間でピンチジェスチャを検出することは、思ったほど簡単ではありませんが、HammerJSを使用するとはるかに簡単になります。

HammerJSデモでピンチズームとパンをチェックしてください。この例は、Android、iOS、およびWindowsPhoneでテストされています。

ソースコードは、Pinch Zoom and Pan withHammerJSにあります。

あなたの便宜のために、ここにソースコードがあります:

<!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>


3

Hammer.jsのようなサードパーティのライブラリを使用すると、任意の要素を2本の指でピンチズームするのが簡単で手間がかかりません(ハンマーにはスクロールの問題があることに注意してください)。

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);
}

よりも使用する方が良いようevent.touchesですevent.targetTouches
TheStoryCoder

1

これらの答えはどれも私が探していたものを達成しなかったので、私は自分で何かを書くことになりました。MacBookProトラックパッドを使用して、Webサイトの画像をピンチズームしたかったのです。次のコード(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'
                    });
                }
            }
        }
    });
}

0

私の答えはジェフリーの答えに触発されています。その答えがより抽象的な解決策を与える場合、私はそれを潜在的に実装する方法についてより具体的なステップを提供しようとします。これは単なるガイドであり、よりエレガントに実装できます。より詳細な例については、MDNWebドキュメントによるこのチュートリアルを確認してください

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>
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.