Najprostszy sposób na wykrycie szczypania

85

To jest aplikacja internetowa, a nie natywna. Proszę nie używać poleceń Objective-C NS.

Muszę więc wykryć zdarzenia „szczypania” na iOS. Problem polega na tym, że każda wtyczka lub metoda, którą widzę do wykonywania gestów lub zdarzeń wielodotykowych, jest (zwykle) z jQuery i jest dodatkową wtyczką dla każdego gestu pod słońcem. Moja aplikacja jest ogromna i jestem bardzo wrażliwy na posadzkę w moim kodzie. Wszystko, czego potrzebuję, to wykryć szczypanie, a używanie czegoś takiego jak jGesture jest po prostu sposobem na nadęty dla moich prostych potrzeb.

Ponadto mam ograniczone pojęcie o ręcznym wykrywaniu szczypania. Mogę ustalić pozycję obu palców, nie mogę dobrze wymieszać, aby to wykryć. Czy ktoś ma prosty fragment, który TYLKO wykrywa szczypanie?

Fresheyeball
źródło

Odpowiedzi:

71

Chcesz użyć gesturestart, gesturechangeoraz gestureendwydarzenia . Uruchamiają się one za każdym razem, gdy co najmniej 2 palce dotykają ekranu.

W zależności od tego, co musisz zrobić z gestem uszczypnięcia, Twoje podejście będzie wymagało dostosowania. scaleMnożnik może być badane w celu określenia, jak dramatyczny gest szczypta autora było. Zobacz dokumentację Apple TouchEvent, aby uzyskać szczegółowe informacje na temat zachowania scalewłaściwości.

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

Możesz także przechwycić gesturechangezdarzenie, aby wykryć szczypanie, jeśli jest to potrzebne, aby aplikacja działała szybciej.

Dan Herbert
źródło
58
Wiem, że to pytanie dotyczyło systemu iOS, ale jego tytuł brzmi: „Najprostszy sposób na wykrycie szczypania”. Zdarzenia Geststart, Gestchange i Gestendend są specyficzne dla systemu iOS i nie działają na wielu platformach. Nie będą uruchamiane na Androidzie ani w żadnej innej przeglądarce dotykowej. Aby to zrobić dla wielu platform, użyj zdarzeń touchstart, touchmove i touchend, jak w tej odpowiedzi stackoverflow.com/a/11183333/375690 .
Phil McCullick
6
@phil Jeśli szukasz najprostszego sposobu na obsługę wszystkich przeglądarek mobilnych, lepiej użyj hammer.js
Dan Herbert
4
Użyłem jQuery $(selector).on('gestureend',...)i musiałem użyć e.originalEvent.scalezamiast e.scale.
Chad von Nau,
3
@ChadvonNau Dzieje się tak, ponieważ obiekt zdarzenia jQuery jest „znormalizowanym obiektem zdarzenia W3C”. Obiekt zdarzenia W3C nie zawiera scalewłaściwości. Jest to właściwość specyficzna dla dostawcy. Chociaż moja odpowiedź zawiera najprostszy sposób wykonania zadania za pomocą waniliowego JS, jeśli już używasz frameworków JS, lepiej byłoby użyć hammer.js, ponieważ zapewni on znacznie lepsze API.
Dan Herbert
1
@superuberduper IE8 / 9 w ogóle nie ma możliwości wykrycia szczypania. Dotykowe interfejsy API zostały dodane do IE dopiero w IE10. Pierwotne pytanie dotyczyło systemu iOS, ale aby poradzić sobie z tym w różnych przeglądarkach, należy użyć struktury hammer.js, która usuwa różnice między przeglądarkami.
Dan Herbert,
135

Pomyśl o tym, czym jest pinchzdarzenie: dwoma palcami na elemencie, zbliżającymi się lub oddalającymi od siebie. Zdarzenia związane z gestami to, o ile wiem, dość nowy standard, więc prawdopodobnie najbezpieczniejszym sposobem jest użycie takich zdarzeń dotykowych:

( ontouchstartwydarzenie)

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

( ontouchmovewydarzenie)

if (scaling) {
    pinchMove(e);
}

( ontouchendwydarzenie)

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

Aby uzyskać odległość między dwoma palcami, użyj hypotfunkcji:

var dist = Math.hypot(
    e.touches[0].pageX - e.touches[1].pageX,
    e.touches[0].pageY - e.touches[1].pageY);
Jeffrey Sweeney
źródło
1
Dlaczego miałbyś pisać własne wykrywanie szczypania? Jest to natywna funkcjonalność zestawu internetowego iOS. Nie jest to również dobra implementacja, ponieważ nie odróżnia przesunięcia dwoma palcami od szczypania. Niezbyt dobra rada.
mmaclaurin
34
@mmaclaurin, ponieważ webkit nie zawsze miał wykrywanie szczypania (popraw mnie, jeśli się mylę), nie wszystkie ekrany dotykowe używają webkita, a czasami zdarzenie przesunięcia nie musi być wykrywane. OP potrzebował prostego rozwiązania bez funkcji biblioteki martwego drewna.
Jeffrey Sweeney
6
OP wspomniał o iOS, ale to najlepsza odpowiedź, biorąc pod uwagę inne platformy. Z wyjątkiem tego, że opuściłeś pierwiastek kwadratowy z obliczania odległości. Włożyłem to.
undefined
3
@BrianMortenson To było zamierzone; sqrtmoże być kosztowne i zazwyczaj wystarczy wiedzieć, że palce przesunęły się do środka lub na zewnątrz o pewną wielkość. Ale ... powiedziałem twierdzenie Pitagorasa i technicznie nie używałem go;)
Jeffrey Sweeney.
2
@mmaclaurin Po prostu sprawdź, czy (deltaX * deltaY <= 0) w ten sposób wykryjesz wszystkie przypadki szczypania, a nie przesunięcie dwoma palcami.
Dolma
29

Hammer.js do końca! Obsługuje „transformacje” (szczypanie). http://eightmedia.github.com/hammer.js/

Ale jeśli chcesz to wdrożyć samodzielnie, myślę, że odpowiedź Jeffreya jest całkiem solidna.

Bruno
źródło
Właśnie znalazłem hammer.js i zaimplementowałem go, zanim zobaczyłem odpowiedź Dana. Młot jest całkiem fajny.
Fresheyeball
Wyglądało to fajnie, ale dema nie były takie gładkie. Powiększanie, a następnie próba przesuwania było naprawdę szarpane.
Alex K
3
Warto zauważyć, że Hammer ma mnóstwo zaległych błędów, z których niektóre są dość poważne w chwili pisania tego (w szczególności Android). Po prostu warto o tym pomyśleć.
Pojedynczy podmiot
3
To samo tutaj, buggy. Wypróbowany Hammer, ostatecznie użył rozwiązania Jeffrey'a.
Paul
4

Niestety, wykrywanie gestów szczypania w przeglądarkach nie jest tak proste, jak można by się spodziewać, ale HammerJS znacznie to ułatwia!

Zobacz demo Pinch Zoom i Pan z HammerJS . Ten przykład został przetestowany na Androidzie, iOS i Windows Phone.

Możesz znaleźć kod źródłowy w Pinch Zoom and Pan with HammerJS .

Dla Twojej wygody oto kod źródłowy:

<!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
źródło
3

wykryj dwa palce, aby powiększyć dowolny element, łatwo i bez kłopotów z bibliotekami innych firm, takimi jak Hammer.js (uwaga, młotek ma problemy z przewijaniem!)

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
źródło
Wygląda na to, że lepiej jest użyć event.touchesniż event.targetTouches.
TheStoryCoder
1

Żadna z tych odpowiedzi nie osiągnęła tego, czego szukałem, więc napisałem coś sam. Chciałem powiększyć obraz w mojej witrynie za pomocą gładzika MacBookPro. Poniższy kod (który wymaga jQuery) wydaje się działać przynajmniej w Chrome i Edge. Może przyda się to komuś innemu.

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
źródło
0

Moja odpowiedź jest inspirowana odpowiedzią Jeffreya. Tam, gdzie ta odpowiedź daje bardziej abstrakcyjne rozwiązanie, staram się przedstawić bardziej konkretne kroki, jak potencjalnie je wdrożyć. To jest po prostu przewodnik, który można zastosować bardziej elegancko. Bardziej szczegółowy przykład można znaleźć w tym samouczku opublikowanym w witrynie 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
źródło