วิธีที่ง่ายที่สุดในการตรวจจับการหยิก


87

นี่คือWEB APPไม่ใช่แอปเนทีฟ โปรดไม่มีคำสั่ง Objective-C NS

ดังนั้นฉันต้องตรวจจับเหตุการณ์ 'หยิก' บน iOS ปัญหาคือทุกปลั๊กอินหรือวิธีการที่ฉันเห็นสำหรับการทำท่าทางสัมผัสหรือเหตุการณ์แบบมัลติทัชคือ (โดยปกติ) กับ jQuery และเป็นปลั๊กอินเพิ่มเติมสำหรับทุกท่าทางภายใต้แสงแดด แอปพลิเคชันของฉันมีขนาดใหญ่มากและฉันรู้สึกไวต่อไม้ตายในรหัสของฉัน สิ่งที่ฉันต้องการคือตรวจจับการหยิกและการใช้สิ่งที่คล้ายกับ jGesture เป็นเพียงวิธีเดียวที่จะทำให้บวมสำหรับความต้องการง่ายๆของฉัน

นอกจากนี้ฉันมีความเข้าใจ จำกัด เกี่ยวกับวิธีตรวจจับการหยิกด้วยตนเอง ฉันสามารถหาตำแหน่งของนิ้วทั้งสองได้ดูเหมือนจะไม่สามารถผสมกันเพื่อตรวจจับสิ่งนี้ได้ ใครมีตัวอย่างง่ายๆที่ตรวจจับการหยิก?

คำตอบ:


71

คุณต้องการที่จะใช้gesturestart, gesturechangeและgestureendเหตุการณ์ที่เกิดขึ้น สิ่งเหล่านี้จะถูกกระตุ้นทุกครั้งที่ 2 นิ้วขึ้นไปสัมผัสหน้าจอ

วิธีการของคุณจะต้องได้รับการปรับเปลี่ยนทั้งนี้ขึ้นอยู่กับสิ่งที่คุณต้องทำกับท่าทางการหยิก scaleคูณสามารถตรวจสอบเพื่อกำหนดวิธีการที่น่าทึ่งท่าทางหยิกของผู้ใช้เป็น ดูเอกสาร TouchEvent ของ Appleสำหรับรายละเอียดเกี่ยวกับการทำงานของ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 โดยเฉพาะ แต่ชื่อคำถามเป็นเรื่องทั่วไป "วิธีที่ง่ายที่สุดในการตรวจจับการบีบ" เหตุการณ์การเริ่มต้นท่าทางการเปลี่ยนท่าทางและการส่งท่าทางเป็น iOS เฉพาะและไม่ทำงานข้ามแพลตฟอร์ม พวกเขาจะไม่เริ่มทำงานบน Android หรือเบราว์เซอร์ระบบสัมผัสอื่น ๆ การทำเช่นนี้การใช้งานข้ามแพลตฟอร์ม touchstart ที่ touchmove และเหตุการณ์ touchend เหมือนในคำตอบนี้stackoverflow.com/a/11183333/375690
Phil McCullick

6
@phil หากคุณกำลังมองหาวิธีที่ง่ายที่สุดในการรองรับเบราว์เซอร์มือถือทั้งหมดคุณควรใช้ hammer.js ดีกว่า
Dan Herbert

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 ที่ดีกว่ามาก
Dan Herbert

1
@superuberduper IE8 / 9 ไม่มีทางตรวจจับการหยิกได้เลย Touch API ไม่ได้ถูกเพิ่มไปยัง IE จนกว่า IE10 คำถามเดิมถามเฉพาะเกี่ยวกับ iOS แต่ในการจัดการกับปัญหานี้ในเบราว์เซอร์คุณควรใช้เฟรมเวิร์ก hammer.js ซึ่งแยกความแตกต่างข้ามเบราว์เซอร์ออกไป
Dan Herbert

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 ต้องการวิธีแก้ปัญหาง่ายๆโดยไม่ต้องใช้ฟังก์ชันห้องสมุดที่ตายแล้ว
Jeffrey Sweeney

6
OP ได้กล่าวถึง iOS แต่นี่เป็นคำตอบที่ดีที่สุดเมื่อพิจารณาจากแพลตฟอร์มอื่น ๆ ยกเว้นคุณได้ปล่อยส่วนรากที่สองออกจากการคำนวณระยะทางแล้ว ใส่เข้าไปแล้ว
undefined

3
@BrianMortenson นั่นคือเจตนา; sqrtอาจมีราคาแพงและโดยทั่วไปคุณจำเป็นต้องรู้ว่านิ้วของคุณขยับเข้าหรือออกเพียงเล็กน้อยเท่านั้น แต่ .. ฉันพูดทฤษฎีบทพีทาโกรัสและฉันไม่ได้ใช้มันในทางเทคนิค)
เจฟฟรีย์สวีนีย์

2
@mmaclaurin เพียงตรวจสอบว่า (deltaX * deltaY <= 0) ด้วยวิธีนี้คุณตรวจพบกรณีการบีบทั้งหมดไม่ใช่การปัดสองนิ้ว
Dolma

30

Hammer.js ตลอดทาง! จัดการ "แปลง" (หยิก) http://eightmedia.github.com/hammer.js/

แต่ถ้าคุณต้องการนำไปใช้ด้วยตัวเองฉันคิดว่าคำตอบของเจฟฟรีย์นั้นค่อนข้างมั่นคง


ฉันเพิ่งพบ hammer.js และนำไปใช้จริงก่อนที่ฉันจะเห็นคำตอบของ Dan ค้อนสวยเท่
Fresheyeball

มันดูดี แต่การสาธิตไม่ได้ราบรื่นขนาดนั้น การซูมเข้าแล้วพยายามหมุนไปรอบ ๆ ให้ความรู้สึกแย่มาก
Alex K

3
ที่น่าสังเกตว่า Hammer มีบั๊กที่โดดเด่นจำนวนมากซึ่งบางส่วนค่อนข้างรุนแรงในขณะที่เขียนสิ่งนี้ (โดยเฉพาะ Android) แค่คิดถึง.
Single Entity

3
เหมือนกันที่นี่รถ Tried Hammer จบลงด้วยการใช้วิธีแก้ปัญหาของ Jeffrey
พอล

4

น่าเสียดายที่การตรวจจับท่าทางการบีบนิ้วบนเบราว์เซอร์นั้นไม่ง่ายอย่างที่หวัง แต่ HammerJS ทำให้ง่ายขึ้นมาก!

ลองดูPinch Zoom และ Pan ด้วยการสาธิต HammerJSสาธิต ตัวอย่างนี้ได้รับการทดสอบบน Android, iOS และ Windows Phone

คุณสามารถค้นหาซอร์สโค้ดได้ที่Pinch Zoom และ Pan with HammerJSHammerJS

เพื่อความสะดวกของคุณนี่คือซอร์สโค้ด:

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


4

ตรวจจับการซูมด้วยสองนิ้วในองค์ประกอบใด ๆ ง่ายและไม่ยุ่งยากกับ libs ของบุคคลที่สามเช่น 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'
                    });
                }
            }
        }
    });
}

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>
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.