La forma más sencilla de detectar un pellizco

85

Esta es una APLICACIÓN WEB, no una aplicación nativa. No utilice comandos de Objective-C NS.

Entonces necesito detectar eventos de 'pellizco' en iOS. El problema es que cada complemento o método que veo para hacer gestos o eventos multitáctiles es (generalmente) con jQuery y es un complemento adicional para cada gesto bajo el sol. Mi aplicación es enorme y soy muy sensible a la madera muerta en mi código. Todo lo que necesito es detectar un pellizco, y usar algo como jGesture es simplemente una forma de inflarme para mis necesidades simples.

Además, tengo un conocimiento limitado de cómo detectar un pellizco manualmente. Puedo obtener la posición de ambos dedos, parece que no puedo mezclar bien para detectar esto. ¿Alguien tiene un fragmento simple que SÓLO detecta pellizco?

Fresheyeball
fuente

Respuestas:

71

Desea utilizar los gesturestart, gesturechangey gestureendeventos . Estos se activan cada vez que 2 o más dedos tocan la pantalla.

Dependiendo de lo que necesite hacer con el gesto de pellizcar, deberá ajustar su enfoque. El scalemultiplicador se puede examinar para determinar qué tan dramático fue el gesto de pellizco del usuario. Consulte la documentación de TouchEvent de Apple para obtener detalles sobre cómo scalese comportará la propiedad.

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

También puede interceptar el gesturechangeevento para detectar un pellizco a medida que sucede si lo necesita para que su aplicación se sienta más receptiva.

Dan Herbert
fuente
58
Sé que esta pregunta era específicamente sobre iOS, pero el título de la pregunta es general "La forma más sencilla de detectar un pellizco". Los eventos de inicio de gestos, cambio de gestos y fin de gestos son específicos de iOS y no funcionan entre plataformas. No se activarán en Android ni en ningún otro navegador táctil. Para hacer esto multiplataforma, use los eventos touchstart, touchmove y touchend, como en esta respuesta stackoverflow.com/a/11183333/375690 .
Phil McCullick
6
@phil Si está buscando la forma más sencilla de admitir todos los navegadores móviles, es mejor que use hammer.js
Dan Herbert
4
Usé jQuery $(selector).on('gestureend',...)y tuve que usar en e.originalEvent.scalelugar de e.scale.
Chad von Nau
3
@ChadvonNau Eso es porque el objeto de evento de jQuery es un "objeto de evento W3C normalizado". El objeto de evento W3C no incluye la scalepropiedad. Es una propiedad específica del proveedor. Si bien mi respuesta incluye la forma más sencilla de realizar la tarea con Vanilla JS, si ya está usando marcos JS, sería mejor que usara hammer.js, ya que le proporcionará una API mucho mejor.
Dan Herbert
1
@superuberduper IE8 / 9 no tiene forma de detectar un pellizco en absoluto. Las API táctiles no se agregaron a IE hasta IE10. La pregunta original se hacía específicamente sobre iOS, pero para manejar esto en todos los navegadores, debe usar el marco hammer.js que abstrae las diferencias entre navegadores.
Dan Herbert
134

Piense en lo pinchque es un evento: dos dedos en un elemento, acercándose o alejándose el uno del otro. Los eventos de gestos son, que yo sepa, un estándar bastante nuevo, por lo que probablemente la forma más segura de hacerlo es usar eventos táctiles como este:

( ontouchstartevento)

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

( ontouchmoveevento)

if (scaling) {
    pinchMove(e);
}

( ontouchendevento)

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

Para obtener la distancia entre los dos dedos, use la hypotfunción:

var dist = Math.hypot(
    e.touches[0].pageX - e.touches[1].pageX,
    e.touches[0].pageY - e.touches[1].pageY);
Jeffrey Sweeney
fuente
1
¿Por qué escribirías tu propia detección de pellizcos? Esta es una funcionalidad nativa en el webkit de iOS. Esta tampoco es una buena implementación, ya que no puede diferenciar entre deslizar dos dedos y pellizcar. No es un buen consejo.
mmaclaurin
34
@mmaclaurin porque webkit no siempre tenía detección de pellizcos (corrígeme si me equivoco), no todas las pantallas táctiles usan webkit y, a veces, no es necesario detectar un evento de deslizamiento. El OP quería una solución simple sin funciones de biblioteca de madera muerta.
Jeffrey Sweeney
6
OP mencionó iOS, pero esta es la mejor respuesta al considerar otras plataformas. Excepto que ha dejado la parte de la raíz cuadrada fuera de su cálculo de distancia. Lo puse.
indefinido
3
@BrianMortenson Eso fue intencional; sqrtpuede ser costoso y, por lo general, solo necesita saber que sus dedos se movieron hacia adentro o hacia afuera en cierta magnitud. Pero ... dije el teorema de Pitágoras, y técnicamente no lo estaba usando;)
Jeffrey Sweeney
2
@mmaclaurin Simplemente verifique si (deltaX * deltaY <= 0) de esa manera detecta todos los casos de pellizco y no el deslizamiento de dos dedos.
Dolma
29

Hammer.js hasta el final! Maneja "transforma" (pellizcos). http://eightmedia.github.com/hammer.js/

Pero si desea implementarlo usted mismo, creo que la respuesta de Jeffrey es bastante sólida.

Bruno
fuente
De hecho, acababa de encontrar hammer.js y lo implementé antes de ver la respuesta de Dan. Hammer es genial.
Fresheyeball
Se veía genial, pero las demostraciones no fueron tan fluidas. Acercar y luego tratar de desplazarse se sintió realmente extraño.
Alex K
3
Vale la pena señalar que Hammer tiene una gran cantidad de errores pendientes, algunos de los cuales son bastante graves al momento de escribir esto (Android en particular). Solo vale la pena pensarlo.
Entidad única
3
Lo mismo aquí, buggy. Probé Hammer, terminé usando la solución de Jeffrey.
Paul
4

Desafortunadamente, detectar los gestos de pellizco en los navegadores no es tan simple como cabría esperar, ¡pero HammerJS lo hace mucho más fácil!

Vea la demostración de Pinch Zoom and Pan with HammerJS . Este ejemplo se ha probado en Android, iOS y Windows Phone.

Puede encontrar el código fuente en Pinch Zoom and Pan with HammerJS .

Para su comodidad, aquí está el código fuente:

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

Detecta dos dedos, haz zoom en cualquier elemento, fácil y sin problemas con bibliotecas de terceros como Hammer.js (¡cuidado, el martillo tiene problemas con el desplazamiento!)

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
fuente
Parece que es mejor usar event.touchesque event.targetTouches.
TheStoryCoder
1

Ninguna de estas respuestas logró lo que estaba buscando, así que terminé escribiendo algo yo mismo. Quería pellizcar y hacer zoom en una imagen en mi sitio web usando mi trackpad MacBookPro. El siguiente código (que requiere jQuery) parece funcionar en Chrome y Edge, al menos. Quizás esto sea de utilidad para otra persona.

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
fuente
0

Mi respuesta está inspirada en la respuesta de Jeffrey. Cuando esa respuesta ofrece una solución más abstracta, trato de proporcionar pasos más concretos sobre cómo implementarla potencialmente. Esto es simplemente una guía, una que se puede implementar de manera más elegante. Para obtener un ejemplo más detallado, consulte este tutorial de MDN web docs.

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>
Lázaro-CG
fuente