Самый простой способ обнаружить защемление

86

Это ВЕБ-приложение приложение, а не собственное приложение. Пожалуйста, никаких команд Objective-C NS.

Поэтому мне нужно обнаруживать события «защемления» на iOS. Проблема заключается в том, что каждый плагин или метод, которые я вижу для выполнения жестов или событий multi-touch, (обычно) использует jQuery и представляет собой целый дополнительный плагин для каждого жеста под солнцем. Мое приложение огромно, и я очень чувствителен к мертвецам в моем коде. Все, что мне нужно, это обнаружить щепотку, а использование чего-то вроде jGesture - это просто способ раздутия для моих простых нужд.

Кроме того, у меня ограниченное представление о том, как обнаружить защемление вручную. Я могу определить положение обоих пальцев, но не могу правильно определить микс. У кого-нибудь есть простой фрагмент, который ТОЛЬКО обнаруживает защемление?

Fresheyeball
источник

Ответы:

71

Вы хотите использовать gesturestart, gesturechangeи gestureendсобытие . Они срабатывают каждый раз, когда 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событие, чтобы обнаружить защемление, если оно вам нужно, чтобы ваше приложение чувствовало себя более отзывчивым.

Дэн Герберт
источник
59
Я знаю, что этот вопрос касался конкретно iOS, но общий заголовок вопроса - «Самый простой способ обнаружить защемление». События gesturestart, gesturechange и gestureend зависят от iOS и не работают на разных платформах. Они не будут работать на Android или других сенсорных браузерах. Для этого кроссплатформенного использования используйте события touchstart, touchmove и touchend, как в этом ответе stackoverflow.com/a/11183333/375690 .
Phil McCullick
6
@phil Если вы ищете самый простой способ поддержки всех мобильных браузеров, вам лучше использовать hammer.js
Дэн Герберт
4
Я использовал jQuery $(selector).on('gestureend',...), и мне пришлось использовать e.originalEvent.scaleвместо e.scale.
Chad von Nau
3
@ChadvonNau Это потому, что объект события jQuery является «нормализованным объектом события W3C». Объект события W3C не включает scaleсвойство. Это свойство конкретного поставщика. Хотя мой ответ включает простейший способ выполнить задачу с помощью vanilla JS, если вы уже используете JS-фреймворки, вам лучше использовать hammer.js, поскольку он предоставит вам гораздо лучший API.
Дэн Герберт
1
@superuberduper IE8 / 9 вообще не может обнаружить защемление. Сенсорные API не добавлялись в IE до IE10. В исходном вопросе конкретно задан вопрос об iOS, но чтобы справиться с этим в разных браузерах, вы должны использовать фреймворк hammer.js, который абстрагирует кросс-браузерные различия.
Дэн Герберт,
136

Подумайте, что за 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);
Джеффри Суини
источник
1
Зачем вам писать собственное обнаружение защемления? Это встроенная функциональность в iOS webkit. Это также не очень хорошая реализация, так как не видно разницы между проведением двумя пальцами и сведением пальцев. Не лучший совет.
mmaclaurin
34
@mmaclaurin, потому что webkit не всегда обнаруживал защемление (поправьте меня, если я ошибаюсь), не все сенсорные экраны используют webkit, и иногда событие считывания не нужно обнаруживать. OP хотел простое решение без функций библиотеки deadwood.
Джеффри Суини
6
OP упомянул iOS, но это лучший ответ при рассмотрении других платформ. За исключением того, что вы исключили квадратный корень из расчета расстояния. Я положил его.
undefined
3
@BrianMortenson Это было намеренно; sqrtможет быть дорогостоящим, и вам, как правило, нужно только знать, насколько сильно ваши пальцы вошли внутрь или наружу. Но ... Я сказал теорему Пифагора, и технически я ее не использовал;)
Джеффри Суини
2
@mmaclaurin Просто проверьте, обнаруживает ли (deltaX * deltaY <= 0) таким образом все случаи защемления, а не проведение двумя пальцами.
Dolma
30

Hammer.js полностью! Он обрабатывает «трансформирует» (зажимает). http://eightmedia.github.com/hammer.js/

Но если вы хотите реализовать это самостоятельно, я думаю, что ответ Джеффри довольно надежен.

Бруно
источник
На самом деле я только что нашел hammer.js и реализовал его, прежде чем увидел ответ Дэна. Хаммер довольно крутой.
Fresheyeball
Это выглядело круто, но демо не были такими гладкими. Увеличивать масштаб, а затем пытаться панорамировать, было очень неприятно.
Alex K
3
Стоит отметить, что в Hammer есть куча нерешенных ошибок, некоторые из которых на момент написания этой статьи были довольно серьезными (в частности, Android). Просто стоит подумать.
Single Entity
3
То же самое, глючное. Пробовал Хаммер, в итоге использовал решение Джеффри.
Пол
4

К сожалению, обнаружить жесты сжатия в браузерах не так просто, как можно было бы надеяться, но с HammerJS это намного проще!

Посмотрите демонстрацию Pinch Zoom and Pan with 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>

Redgeoff
источник
4

обнаруживайте масштабирование двумя пальцами на любом элементе, легко и без проблем со сторонними библиотеками, такими как Hammer.js (будьте осторожны, у молотка проблемы с прокруткой!)

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);
}
Андрей
источник
1
Вроде лучше использовать event.touchesчем event.targetTouches.
TheStoryCoder
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'
                    });
                }
            }
        }
    });
}
gcdev
источник
0

Мой ответ вдохновлен ответом Джеффри. Там, где этот ответ дает более абстрактное решение, я стараюсь предоставить более конкретные шаги по его возможной реализации. Это просто руководство, которое можно реализовать более элегантно. Более подробный пример можно найти в этом руководстве по веб-документации 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>
Лазарь-CG
источник