Maneira mais simples de detectar uma pinça

86

Este é um aplicativo da web, não um aplicativo nativo. Por favor, nenhum comando Objective-C NS.

Portanto, preciso detectar eventos de 'pinça' no iOS. O problema é que cada plugin ou método que vejo para fazer gestos ou eventos multi-touch é (geralmente) com jQuery e é um plugin adicional para cada gesto sob o sol. Meu aplicativo é enorme e sou muito sensível ao código morto em meu código. Tudo que eu preciso é detectar uma pinça e usar algo como jGesture é apenas uma maneira de inflar para minhas necessidades simples.

Além disso, tenho um conhecimento limitado de como detectar manualmente uma pinça. Consigo obter a posição de ambos os dedos, mas não consigo obter a mistura certa para detectar isto. Alguém tem um snippet simples que APENAS detecta uma pinça?

Fresheyeball
fonte

Respostas:

71

Você quer usar os gesturestart, gesturechangee gestureendeventos . Eles são acionados sempre que 2 ou mais dedos tocam a tela.

Dependendo do que você precisa fazer com o gesto de pinça, sua abordagem precisará ser ajustada. O scalemultiplicador pode ser examinado para determinar o quão dramático foi o gesto de pinça do usuário. Consulte a documentação TouchEvent da Apple para obter detalhes sobre como a scalepropriedade se comportará.

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

Você também pode interceptar o gesturechangeevento para detectar uma pinça quando ela acontece, se precisar fazer seu aplicativo parecer mais responsivo.

Dan Herbert
fonte
59
Eu sei que esta pergunta era especificamente sobre iOS, mas o título da pergunta é geral "A maneira mais simples de detectar uma pinça". Os eventos gesturestart, gesturechange e Gestend são específicos do iOS e não funcionam entre plataformas. Eles não serão disparados no Android ou em qualquer outro navegador de toque. Para fazer isso entre plataformas, use os eventos touchstart, touchmove e touchend, como nesta resposta stackoverflow.com/a/11183333/375690 .
Phil McCullick de
6
@phil Se você está procurando a maneira mais simples de oferecer suporte a todos os navegadores móveis, é melhor usar o hammer.js
Dan Herbert
4
Usei jQuery $(selector).on('gestureend',...)e tive que usar em e.originalEvent.scalevez de e.scale.
Chad von Nau
3
@ChadvonNau Isso ocorre porque o objeto de evento do jQuery é um "objeto de evento W3C normalizado". O objeto W3C Event não inclui a scalepropriedade. É uma propriedade específica do fornecedor. Embora minha resposta inclua a maneira mais simples de realizar a tarefa com o vanilla JS, se você já estiver usando estruturas JS, seria melhor usar o hammer.js, pois ele fornecerá uma API muito melhor.
Dan Herbert
1
@superuberduper IE8 / 9 não tem como detectar uma pinça. As APIs de toque não foram adicionadas ao IE até o IE10. A pergunta original perguntava especificamente sobre o iOS, mas para lidar com isso em todos os navegadores, você deve usar a estrutura do hammer.js que abstrai as diferenças entre navegadores.
Dan Herbert
136

Pense no que é um pinchevento: dois dedos em um elemento, aproximando-se ou afastando-se um do outro. Os eventos de gestos são, até onde sei, um padrão relativamente novo, então provavelmente a maneira mais segura de fazer isso é usar eventos de toque como:

( ontouchstartevento)

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

( ontouchmoveevento)

if (scaling) {
    pinchMove(e);
}

( ontouchendevento)

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

Para obter a distância entre os dois dedos, use a hypotfunção:

var dist = Math.hypot(
    e.touches[0].pageX - e.touches[1].pageX,
    e.touches[0].pageY - e.touches[1].pageY);
Jeffrey Sweeney
fonte
1
Por que você escreveria sua própria detecção de pinça? Esta é uma funcionalidade nativa no webkit iOS. Essa também não é uma boa implementação, pois não consegue diferenciar entre um toque e um beliscão de dois dedos. Não é um bom conselho.
mmaclaurin
34
@mmaclaurin porque o webkit nem sempre tem detecção de pinça (corrija-me se eu estiver errado), nem todas as telas sensíveis ao toque usam webkit e, às vezes, um evento de deslizar não precisa ser detectado. O OP queria uma solução simples sem funções de biblioteca deadwood.
Jeffrey Sweeney
6
OP mencionou iOS, mas esta é a melhor resposta quando se considera outras plataformas. Exceto que você deixou a raiz quadrada de fora do cálculo da distância. Eu coloquei.
indefinido
3
@BrianMortenson Isso foi intencional; sqrtpode ser caro, e geralmente você só precisa saber se seus dedos se moveram para dentro ou para fora em alguma magnitude. Mas ... Eu disse teorema de Pitágoras, e não o estava usando tecnicamente;)
Jeffrey Sweeney
2
@mmaclaurin Apenas verifique se (deltaX * deltaY <= 0) dessa forma você detecta todos os casos de pinça e não o toque de dois dedos.
Dolma
30

Hammer.js até o fim! Ele lida com "transformações" (beliscões). http://eightmedia.github.com/hammer.js/

Mas se você deseja implementá-lo sozinho, acho que a resposta de Jeffrey é bastante sólida.

Bruno
fonte
Na verdade, eu tinha acabado de encontrar o hammer.js e implementá-lo antes de ver a resposta de Dan. O martelo é muito legal.
Fresheyeball
Pareceu legal, mas as demos não foram tão suaves. Aproximar o zoom e tentar deslocar ao redor parecia muito desajeitado.
Alex K,
3
Vale a pena notar que o Hammer tem uma grande quantidade de bugs pendentes, alguns dos quais são bastante graves no momento em que este livro foi escrito (Android em particular). Só vale a pena pensar.
Entidade Única
3
O mesmo aqui, bugado. Martelo tentado, acabou usando a solução de Jeffrey.
Paul
4

Infelizmente, detectar gestos de pinça em navegadores não é tão simples quanto se esperava, mas o HammerJS torna isso muito mais fácil!

Confira a demonstração do Pinch Zoom e Pan com HammerJS . Este exemplo foi testado em Android, iOS e Windows Phone.

Você pode encontrar o código-fonte em Pinch Zoom and Pan with HammerJS .

Para sua conveniência, aqui está o código-fonte:

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

redgeoff
fonte
4

detectar dois dedos apertar o zoom em qualquer elemento, fácil e sem problemas com bibliotecas de terceiros, como Hammer.js (cuidado, o martelo tem problemas com a rolagem!)

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);
}
Andrey
fonte
1
Parece que é melhor usar do event.touchesque event.targetTouches.
TheStoryCoder
1

Nenhuma dessas respostas alcançou o que eu procurava, então acabei escrevendo algo sozinho. Eu queria dar zoom em uma imagem no meu site usando o trackpad do MacBookPro. O código a seguir (que requer jQuery) parece funcionar no Chrome e no Edge, pelo menos. Talvez isso seja útil para outra pessoa.

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'
                    });
                }
            }
        }
    });
}
gcdev
fonte
0

Minha resposta é inspirada na resposta de Jeffrey. Onde essa resposta fornece uma solução mais abstrata, tento fornecer etapas mais concretas sobre como potencialmente implementá-la. Este é apenas um guia, que pode ser implementado de forma mais elegante. Para um exemplo mais detalhado, confira este tutorial por documentos da web do MDN.

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>
Lazarus-CG
fonte